using System; using System.IO; using UnityEngine; using UnityEngine.UI; using AssetBundles; using System.Collections; using System.Collections.Generic; using System.Text; using MeshVR; using MVR.FileManagementSecure; using SimpleJSON; using System.Text.RegularExpressions; using Battlehub.RTSaveLoad; using Battlehub.RTCommon; using Leap.Unity; using UnityEngine.EventSystems; namespace Stopper { public class AlternativeFuta : MVRScript { private string date = "Version 2026-06-29"; string packageId; string packagePath; string altFutaSavePath = "Saves\\PluginData\\RunRudolf\\AlternativeFuta"; string altFutaFileExt = "altfuta"; // Character skin prefabs GameObject femaleSkinsPrefab; GameObject maleSkinsPrefab; GameObject femaleSkinsContainer; GameObject maleAsset; // Stuff for the new 'DAZCharacter' that has our new modified skin GameObject newCharacterContainer; DAZCharacter newCharacter; DAZCharacter previousSelected; DAZCharacterSelector characterSelector; DAZCharacterRun characterRun; DAZMergedSkinV2 skin; DAZMergedMesh mergedMesh; DAZMesh genMesh; GameObject femaleSkinBase; DAZBone gen1Bone; DAZBone gen2Bone; DAZBone gen3Bone; string futaSkinName = "AltFutaSkin"; Dictionary femaleSkinsWithMatchingFuta = new Dictionary() { { "renkayla", "FutaKayla" }, { "renlexi", "FutaLexi" }, { "renmaria", "FutaMaria" }, { "renmia", "FutaMia" }, { "rensimone", "FutaSimone" }, }; // We also instantiate a male character to steal some data from during initialization GameObject maleCharacterContainer; GameObject maleSkinBase; DAZMergedSkinV2 maleSkin; DAZMergedMesh maleMergedMesh; List maleObjects = new List(); AltFutaMarker marker; JSONStorableString status; // UI Stuff GameObject UIContainer; GameObject materialUIPrefab; MVRScriptUI scriptUI; JSONStorableBool hideFemaleGens; JSONStorableBool hideMalePelvis; JSONStorableBool useTransparentShader; JSONStorableBool mergeLoad; JSONStorableBool disableTestesCollision; JSONStorableBool showMaterial29; //Pelvis JSONStorableBool showMaterial30; //Penis bool skinLoaded = false; bool useVaginaAsPelvisBase = false; bool overrideFemalePelvisTextures = false; // Mat Options MaterialOptionsWrapper genMaterialOptions; Transform genMaterialOptionsUI; // Morph Stuff float genBoneYOffsetDefault = 0.0223f; float genBoneZOffsetDefault = -0.010f; float hidePerineumDefault = 0f;// 0.5f; DAZMorph genYOffsetMorph; DAZMorph genZOffsetMorph; DAZMorph genYMeshOffsetMorph; DAZMorph genZMeshOffsetMorph; DAZMorph hidePerineumMorph; DAZMorph gapFixMorph; DAZMorph vaginaHideMorph; DAZMorph pelvisMorph1; DAZMorph pelvisMorph2; DAZMorph pelvisMorph3; List morphs = new List(); JSONStorableFloat genYOffsetValue; JSONStorableFloat genZOffsetValue; JSONStorableFloat genYMeshOffsetValue; JSONStorableFloat genZMeshOffsetValue; JSONStorableFloat hidePerineumOffsetValue; JSONStorableFloat gapFixOffsetValue; JSONStorableFloat vaginaHideOffsetValue; JSONStorableFloat hiddenPenisLengthValue; JSONStorableFloat pelvisMorph1Value; JSONStorableFloat pelvisMorph2Value; JSONStorableFloat pelvisMorph3Value; // Seam color blending JSONStorableBool seamColorBlendEnabled; const float SeamColorBlendRadius = 0.1f; // Hide-pelvis mode: how far up the shaft (above the body surface) the // body-color fade reaches before the penis texture takes over. const float HiddenSeamFadeRadius = 0.04f; // Hide-pelvis mode: graft verts farther than this from any visible body // vertex are left unpainted. const float HiddenSeamSearchRange = 0.06f; // Hide-pelvis mode calibration sliders (temporary: once good values are // found they get hard-coded and the sliders removed) JSONStorableFloat seamFadeRange; JSONStorableFloat seamAnchorOffset; JSONStorableFloat seamColorBrightness; JSONStorableFloat seamSurfaceRange; JSONStorableFloat seamPatchRadius; // Hide-pelvis surface map fade (spec/gloss/normal detail removal at the // base): original textures by property name, and our baked replacements Dictionary _seamSurfPrev = new Dictionary(); List _seamSurfTextures = new List(); bool _seamSurfNulled; bool _skinColorMatched; HSVColor _skinColorPrev; Texture2D _seamColorDecalTex; Texture _seamDecalPrevPelvis; Texture _seamDecalPrevPenis; bool _seamDecalApplied; Dictionary _bodySeamDecalPrev = new Dictionary(); List _bodySeamDecalTextures = new List(); // Baking is heavy, so UI callbacks only schedule a rebake here; Update() // runs it once the value has settled (and only once the skin is loaded). float _seamRebakeAt = -1f; // Right after a skin reload textures are still loading asynchronously, so // a bake can fail (retry) or get silently overwritten by a texture that // finishes loading later (integrity watch). int _seamRebakeRetries; float _seamWatchAt; // Saving and loading JSONClass savedPresetJson; JSONClass[] oldSkinOpts; JSONClass oldTextureControlJson; // Physics stuff List genColliderIgnoreNames = new List { // Front pelvis "AutoColliderpelvisF1","AutoColliderpelvisF2","AutoColliderpelvisF3", "AutoColliderpelvisF4","AutoColliderpelvisF5","AutoColliderpelvisF6", "pelvisF7", "pelvisF8", "pelvisF9", // Inner pelvis "AutoColliderpelvisFL1","AutoColliderpelvisFL1b", "AutoColliderpelvisFL2","AutoColliderpelvisFL3","AutoColliderpelvisFL4","AutoColliderpelvisFL5", "AutoColliderpelvisFR1","AutoColliderpelvisFR1b", "AutoColliderpelvisFR2","AutoColliderpelvisFR3","AutoColliderpelvisFR4","AutoColliderpelvisFR5", // Vagina "JointG", "Collider1" }; List genAutoColliderIgnoreList = new List(); List genColliderIgnoreList = new List(); HashSet genAutoColliders = new HashSet(); HashSet genColliders = new HashSet(); Dictionary maleAutoColliderConfigs = new Dictionary(); Dictionary morphValues = new Dictionary(); // RunRudolf JSONStorableAction reloadSkinAction; JSONStorableAction loadPresetAction; JSONStorableAction savePresetAction; List descriptionTextFields = new List(); List ignoreLayoutUI = new List(); JSONStorableString pathInput; // Triggered preset loading with path string pluginID1; bool isFemale; bool isLoaded = false; DAZCharacterSelector geometry; DAZCharacterSelector Selector; DAZMorph penisLengthMorph; DAZMorph scCastrateMorph; DAZMorph genitalBulge1Morph; DAZMorph genitalAreaInOutMorph; DAZMorph monsPubicInOutMorph; JSONStorableFloat genZMeshOffsetValue_restore; JSONStorableFloat genYMeshOffsetValue_restore; JSONStorableFloat penisLengthMorph_restore; JSONStorableFloat scCastrateMorph_restore; JSONStorableFloat genitalBulge1Morph_restore; JSONStorableFloat genitalAreaInOutMorph_restore; JSONStorableFloat monsPubicInOutMorph_restore; JSONStorableFloat pelvisMorph1_restore; JSONStorableFloat pelvisMorph2_restore; JSONStorableFloat pelvisMorph3_restore; JSONStorableBool disableTestesCollision_restore; // True while the hidden-penis morph override is applied. Guards the // capture/restore logic: without it, loading or destroying the plugin // clobbers the character's own morph values with stale restore values. JSONStorableBool hiddenMorphsApplied; UIDynamicSlider hiddenPenisLengthSlider; UIDynamicSlider genYOffsetValueSlider; UIDynamicSlider genZOffsetValueSlider; UIDynamicSlider hidePerineumOffsetValueSlider; UIDynamicSlider vaginaHideOffsetValueSlider; UIDynamicSlider genYMeshOffsetValueSlider; UIDynamicSlider genZMeshOffsetValueSlider; UIDynamicSlider gapFixOffsetValueSlider; UIDynamicSlider pelvisMorph1ValueSlider; UIDynamicSlider pelvisMorph2ValueSlider; UIDynamicSlider pelvisMorph3ValueSlider; public class AutoColliderConfig { public int targetVertex = -1; public int anchorVertex1 = -1; public int anchorVertex2 = -1; public int oppositeVertex = -1; } // Small wrappers that help us get access to some protected members public class DAZSkinV2Wrapper : DAZSkinV2 { public DAZSkinV2Wrapper() { this.GPUmaterials = new Material[0]; } public void Copy(DAZSkinV2 other) { this.dazMesh = other.dazMesh; this.sceneGeometryId = other.sceneGeometryId; this._numBones = other.numBones; this.nodes = other.nodes; } } // This is used to detect when the selected character is changed public class DisableListener : MonoBehaviour { public AlternativeFuta script; public void OnDisable() { if(script == null) return; // Restore modified auto colliders if(script.maleAutoColliderConfigs != null) { foreach(var entry in script.maleAutoColliderConfigs) { AutoCollider ac = entry.Key; AutoColliderConfig config = entry.Value; ac.targetVertex = config.targetVertex; ac.anchorVertex1 = config.anchorVertex1; ac.anchorVertex2 = config.anchorVertex2; ac.oppositeVertex = config.oppositeVertex; ac.AutoColliderSizeSet(true); } } } } public class AltFutaMarker : JSONStorable { public void Register(Atom atom) { atom.RegisterAdditionalStorable(this); } public void Unregister() { if(containingAtom != null) containingAtom.UnregisterAdditionalStorable(this); } } public class MaterialOptionsWrapper : DAZCharacterMaterialOptions { public void SetParams() { this.SetAllParameters(); } public void CopyMaterialParams(DAZCharacterMaterialOptions src) { if(color1JSONParam != null) color1JSONParam.SetColor(src.color1CurrentColor); if(color2JSONParam != null) color2JSONParam.SetColor(src.color2CurrentColor); if(color3JSONParam != null) color3JSONParam.SetColor(src.color3CurrentColor); if(param1JSONParam != null) param1JSONParam.val = src.param1CurrentValue; if(param2JSONParam != null) param2JSONParam.val = src.param2CurrentValue; if(param3JSONParam != null) param3JSONParam.val = src.param3CurrentValue; if(param4JSONParam != null) param4JSONParam.val = src.param4CurrentValue; if(param5JSONParam != null) param5JSONParam.val = src.param5CurrentValue; // Param indexes don't match past this point if(param6JSONParam != null) param6JSONParam.val = src.param9CurrentValue; // Param 7 is Alpha adjust if(param8JSONParam != null) param8JSONParam.val = src.param6CurrentValue; if(param9JSONParam != null) param9JSONParam.val = src.param7CurrentValue; if(param10JSONParam != null) param10JSONParam.val = src.param8CurrentValue; SetParams(); } public void ClearCustomTextures() { if(customTexture1UrlJSON != null) customTexture1UrlJSON.Clear(); if(customTexture2UrlJSON != null) customTexture2UrlJSON.Clear(); if(customTexture3UrlJSON != null) customTexture3UrlJSON.Clear(); if(customTexture4UrlJSON != null) customTexture4UrlJSON.Clear(); if(customTexture5UrlJSON != null) customTexture5UrlJSON.Clear(); if(customTexture6UrlJSON != null) customTexture6UrlJSON.Clear(); } } public override void Init() { // Initial Checks: Only load on person atom if (containingAtom.type != "Person") { SuperController.LogError("AlternativeFuta: Please add the plugin to a Person atom."); return; } // Initial Checks: Only load one plugin instance pluginID1 = FindPluginID(containingAtom, "RunRudolf.AlternativeFuta"); if (pluginID1 != null) { SuperController.LogError("AlternativeFuta: Please load only one instance of this plugin for each atom."); return; } // Initial Checks: Only load the plugin on female atoms geometry = (DAZCharacterSelector)containingAtom.GetStorableByID("geometry"); isFemale = geometry.gender == DAZCharacterSelector.Gender.Female; if (!isFemale) { SuperController.LogError("AlternativeFuta: Please load this plugin on a female person atom."); return; } // Get Package ID packageId = GetPackageId(this); packagePath = string.IsNullOrEmpty(packageId) ? "" : $"{packageId}:/"; marker = this.gameObject.AddComponent(); marker.overrideId = "AltFuta"; status = new JSONStorableString("status", "initializing"); status.isStorable = false; // We don't want to store this marker.RegisterString(status); marker.Register(containingAtom); // Initialize tooltips Skynet.ToolTipManager.Init(this); // Initialize some references characterRun = geometry.GetComponent(); gen1Bone = characterRun.bones.GetDAZBone("Gen1"); gen2Bone = characterRun.bones.GetDAZBone("Gen2"); gen3Bone = characterRun.bones.GetDAZBone("Gen3"); GetMaleTransforms(); SaveAutoColliderConfigs(); SetMaleTransformsActive(true); characterSelector = geometry.GetComponent(); previousSelected = characterSelector.selectedCharacter; // TODO: Replace these with hard-coded paths foreach (GameObject go in Resources.FindObjectsOfTypeAll(typeof(GameObject)) as GameObject[]){ if(go.name =="FemaleCharactersPrefab") { femaleSkinsPrefab = go; } if(go.name =="MaleCharactersPrefab") { maleSkinsPrefab = go; } } foreach(GameObject go in GetAllChildren(geometry.gameObject)) { if(go.name.Contains("FemaleCharactersPrefab")) { femaleSkinsContainer = go; } } // Select the base skin. We'll try to use whatever selected, otherwise we choose 'rensimone' arbitrarily string baseSkinName = null; try { if (previousSelected != null) { // Store previous material options DAZCharacterMaterialOptions[] oldOptions = previousSelected.GetComponentsInChildren(); oldSkinOpts = new JSONClass[oldOptions.Length]; for(int j = 0; j < oldOptions.Length; j++) { var json = oldOptions[j].GetJSON(true,true,true); oldSkinOpts[j] = json; } // Store other material settings in DAZCharacterTextureControl var oldTextureControl = previousSelected.GetComponentInChildren(); if(oldTextureControl != null) { oldTextureControlJson = oldTextureControl.GetJSON(true,true,true); } baseSkinName = previousSelected.name; femaleSkinBase = previousSelected.gameObject; } else { baseSkinName = "rensimone"; foreach(GameObject go in GetAllChildren(femaleSkinsPrefab)) { if(go.name == baseSkinName) { femaleSkinBase = go; } } } } catch (Exception e) { SuperController.LogError($"Error storing previous skin options. {e}"); } if (femaleSkinBase == null) { SuperController.LogMessage($"Unable to find female skin preset for '{baseSkinName}'"); return; } string maleSkinName = null; femaleSkinsWithMatchingFuta.TryGetValue(baseSkinName, out maleSkinName); if (maleSkinName == null) { maleSkinName = "FutaSimone"; } foreach (GameObject go in GetAllChildren(maleSkinsPrefab)) { if (go.name == maleSkinName) { maleSkinBase = go; } } var physicsModel = containingAtom.GetStorableByID("PhysicsModel"); foreach (AutoCollider ac in physicsModel.GetComponentsInChildren()) { foreach (string toIgnore in genColliderIgnoreNames) { if (ac.name.Contains(toIgnore) || ac.gameObject.transform.parent.name.Contains(toIgnore) ) { genAutoColliderIgnoreList.Add(ac); } } } foreach (Collider c in physicsModel.GetComponentsInChildren()) { foreach (string toIgnore in genColliderIgnoreNames) { if (c.name.Contains(toIgnore) || c.gameObject.transform.parent.name.Contains(toIgnore)) { genColliderIgnoreList.Add(c); } } } InitMainUI(); AddMaleGenitalia(); // Initialize restore values of morphs and testicle collision setting genZMeshOffsetValue_restore = new JSONStorableFloat("genZMeshOffsetValue_restore", 0f, -5f, 5f, true, true); genZMeshOffsetValue_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(genZMeshOffsetValue_restore); genYMeshOffsetValue_restore = new JSONStorableFloat("genYMeshOffsetValue_restore", 0f, -5f, 5f, true, true); genYMeshOffsetValue_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(genYMeshOffsetValue_restore); penisLengthMorph_restore = new JSONStorableFloat("penisLengthMorph_restore", 0f, -5f, 5f, true, true); penisLengthMorph_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(penisLengthMorph_restore); scCastrateMorph_restore = new JSONStorableFloat("scCastrateMorph_restore", 0f, -5f, 5f, true, true); scCastrateMorph_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(scCastrateMorph_restore); genitalBulge1Morph_restore = new JSONStorableFloat("genitalBulge1Morph_restore", 0f, -5f, 5f, true, true); genitalBulge1Morph_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(genitalBulge1Morph_restore); genitalAreaInOutMorph_restore = new JSONStorableFloat("genitalAreaInOutMorph_restore", 0f, -5f, 5f, true, true); genitalAreaInOutMorph_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(genitalAreaInOutMorph_restore); monsPubicInOutMorph_restore = new JSONStorableFloat("monsPubicInOutMorph_restore", 0f, -5f, 5f, true, true); monsPubicInOutMorph_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(monsPubicInOutMorph_restore); pelvisMorph1_restore = new JSONStorableFloat("pelvisMorph1_restore", 0f, -5f, 5f, true, true); pelvisMorph1_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(pelvisMorph1_restore); pelvisMorph2_restore = new JSONStorableFloat("pelvisMorph2_restore", 0f, -5f, 5f, true, true); pelvisMorph2_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(pelvisMorph2_restore); pelvisMorph3_restore = new JSONStorableFloat("pelvisMorph3_restore", 0f, -5f, 5f, true, true); pelvisMorph3_restore.storeType = JSONStorableParam.StoreType.Full; RegisterFloat(pelvisMorph3_restore); disableTestesCollision_restore = new JSONStorableBool("disableTestesCollision_restore", false); disableTestesCollision_restore.storeType = JSONStorableParam.StoreType.Full; RegisterBool(disableTestesCollision_restore); hiddenMorphsApplied = new JSONStorableBool("hiddenMorphsApplied", false); hiddenMorphsApplied.storeType = JSONStorableParam.StoreType.Full; RegisterBool(hiddenMorphsApplied); // Initialise triggerable actions reloadSkinAction = new JSONStorableAction("Reload Skin", ReloadSkin); RegisterAction(reloadSkinAction); loadPresetAction = new JSONStorableAction("Load Preset", loadPreset); RegisterAction(loadPresetAction); savePresetAction = new JSONStorableAction("Save Preset", savePreset); RegisterAction(savePresetAction); // Triggered preset loading pathInput = new JSONStorableString("Load Preset From Path", "", (string val) => { //SuperController.LogMessage("Received path: " + val); loadPresetWithPath(val); }); RegisterString(pathInput); StartCoroutine(WaitForLoadingComplete()); } // Wait until skin is loaded before doing the UV check private IEnumerator WaitForLoadingComplete() { while (skinLoaded == false) { yield return null; } checkUVsetName(); isLoaded = true; } private string GetHierarchyPath(Transform t) { if (t == null) return ""; string path = t.name; while (t.parent != null) { t = t.parent; path = t.name + "/" + path; } return path; } // Check if UV set is 'Base Female', otherwise throw log message private void checkUVsetName() { DAZCharacterTextureControl textureControl = containingAtom.GetStorableByID("textures") as DAZCharacterTextureControl; if (textureControl != null) { var UVset = textureControl.uvSetName; if (UVset != null && UVset != "Base Female") { SuperController.LogError($"AlternativeFuta: Select a skin set based on 'UV: Base Female' before adding the AlternativeFuta plugin. Other UV's will create texturing errors."); } } } private void GetMaleTransforms() { var physicsModel = containingAtom.GetStorableByID("PhysicsModel"); if (physicsModel == null) return; // List of targets to be looked for var targetNames = new HashSet { "Gen1", "Gen2", "Gen3", "Testes" }; // Get the transforms var allTransforms = physicsModel.gameObject.GetComponentsInChildren(true); foreach (var t in allTransforms) { if (targetNames.Contains(t.name)) { var go = t.gameObject; maleObjects.Add(go); genAutoColliders.UnionWith(go.GetComponentsInChildren(true)); genColliders.UnionWith(go.GetComponentsInChildren(true)); } } // Add genital controls string[] controlIDs = { "penisTipControl", "penisMidControl", "penisBaseControl", "testesControl" }; foreach (var id in controlIDs) { var storable = containingAtom.GetStorableByID(id); if (storable != null && storable.gameObject != null) { maleObjects.Add(storable.gameObject); } } } private void SaveAutoColliderConfigs() { foreach(AutoCollider ac in genAutoColliders) { maleAutoColliderConfigs[ac] = new AutoColliderConfig { targetVertex = ac.targetVertex, anchorVertex1 = ac.anchorVertex1, anchorVertex2 = ac.anchorVertex2, oppositeVertex = ac.oppositeVertex, }; } } // Startup routine public void Start() { StartCoroutine("WaitForLoad"); StartCoroutine("InitMaterialUI"); } private void ReloadSkin() { if (skinLoaded) { skinLoaded = false; StartCoroutine("ReloadSkinAsync"); } } private IEnumerator ReloadSkinAsync() { while(!characterSelector.selectedCharacter.ready) { yield return null; } savedPresetJson = GetJSON(true,true,true); RestoreFromJSON(savedPresetJson); UnloadSkin(); yield return null; AddMaleGenitalia(); StartCoroutine("WaitForLoad"); StartCoroutine("InitMaterialUI"); } private GameObject LoadAssetBundleSync(string assetBundleName, string assetName) { GameObject asset = null; string error; var loadedAssetBundle = AssetBundleManager.GetLoadedAssetBundle(assetBundleName, out error); if(loadedAssetBundle == null) { AssetBundleLoader loader = new AssetBundleLoader(); loader.Load(assetBundleName, (bundleName, assetBundle) => { var loadedAsset = assetBundle.LoadAsset(assetName, typeof(GameObject)) as GameObject; asset = UnityEngine.Object.Instantiate(loadedAsset); assetBundle.Unload(true); }); } else { asset = UnityEngine.Object.Instantiate(loadedAssetBundle.m_AssetBundle.LoadAsset(assetName, typeof(GameObject)) as GameObject); } return asset; } private void AddMaleGenitalia() { SetMaleTransformsActive(true); /* The VAM skin hierarchy includes a DAZMergedSkinV2, which uses a DAZMergedMesh. 'Merged' here refers to the fact that genitals have their own DAZMesh that is grafted onto the main mesh. There is code for 2 graft meshes and, luckily, the female model only uses one. The first thing we'll do is set up a new skin. This requires a few high-level steps: 1. Create a new DAZCharacter off of a prefab - This will create most of our DAZMergedMesh and our DAZMergedSkinV2 2. Add a DAZMesh and DAZSkinV2 for the penis prior to the main skin initializing - The DAZMesh we get from a file - The DAZSkinV2 we'll copy from a male skin 3. Merge the DAZMergedMesh and our DAZMergedSkinV2 4. Set the materials */ // Initialize the male skin that we'll steal some data from. // Specifically, we want the node weights, which determine how each vertex is weiged to the DAZBones of the skeleton maleCharacterContainer = UnityEngine.Object.Instantiate(maleSkinBase); // Enable the object. This will cause the DAZCharacter to load skin artifacts from assetBundles maleCharacterContainer.SetActive(true); DAZCharacter maleCharacter = maleCharacterContainer.GetComponentInChildren(true); if(maleAsset == null) { maleAsset = LoadAssetBundleSync(maleCharacter.assetBundleName, maleCharacter.assetName); maleAsset.SetActive(false); maleAsset.name = "AltFutaMalePrefab"; maleAsset.transform.parent = containingAtom.transform; } DAZMergedSkinV2 maleMergedSkin = maleAsset.GetComponentInChildren(true); maleMergedSkin.enabled = true; maleMergedSkin.CopyMaterials(); maleMergedMesh = (maleMergedSkin.dazMesh as DAZMergedMesh); // Initialize the new skin, which will initially include just the DAZCharacter femaleSkinBase.SetActive(true); newCharacterContainer = UnityEngine.Object.Instantiate(femaleSkinBase, femaleSkinsContainer.transform); femaleSkinBase.SetActive(false); newCharacterContainer.SetActive(false); newCharacterContainer.name = futaSkinName; var disableListener = newCharacterContainer.AddComponent(); disableListener.script = this; newCharacter = newCharacterContainer.GetComponentInChildren(); newCharacter.containingAtom = containingAtom; newCharacter.rootBonesForSkinning = characterSelector.rootBones; if(newCharacterContainer.GetComponentInChildren() == null) { newCharacter.onLoadedHandlers = () => { ModifyAndMergeSkin(); }; } else { ModifyAndMergeSkin(); } // Set the new character as selected try { characterSelector.selectedCharacter = newCharacter; } catch (Exception e) { SuperController.LogError($"Error switching to new skin. {e}"); } } public void ModifyAndMergeSkin() { skin = newCharacterContainer.GetComponentInChildren(true); // Skin should be loaded (but not initialized) by this point mergedMesh = (skin.dazMesh as DAZMergedMesh); skin.name = futaSkinName; // Load our mesh from file. This is the only way I found to initialize the // vertices manually. // The file I'm using came from using StoreToBinaryFile, which required me to // disable file IO restrictions. (Luckily LoadFromBinaryFile doesn't have those) genMesh = mergedMesh.gameObject.AddComponent(); genMesh.LoadFromBinaryFile($"{GetPluginPath(this,containingAtom)}/male_gen_dazmesh"); // The meshgraft includes instructions on which vertices should // be 'glued' to the base model. We're not bothering with this for now. // TODO: Set this... genMesh.meshGraft = new DAZMeshGraft(); genMesh.meshGraft.hiddenPolys = (int[]) maleMergedMesh.graftMesh.meshGraft.hiddenPolys.Clone(); genMesh.meshGraft.vertexPairs = (DAZMeshGraftVertexPair[]) maleMergedMesh.graftMesh.meshGraft.vertexPairs.Clone(); // Don't bother grafting the pelvis to the main body to prevent it deforming if(!hideMalePelvis.val) { List graftPairs = new List(); foreach(var pair in genMesh.meshGraft.vertexPairs) { if(!RemovePins.Contains(pair.vertexNum) && !RemovePins.Contains(pair.graftToVertexNum)) { graftPairs.Add(pair); } } int genVertCount = genMesh.baseVertices.Length; int targetVertCount = mergedMesh.targetMesh.baseVertices.Length; var perineumPins = hideFemaleGens.val ? PerineumPins : PerineumPinsShowVagina; foreach(var entry in perineumPins) { if(entry.Key < 0 || entry.Key >= genVertCount || entry.Value < 0 || entry.Value >= targetVertCount) { SuperController.LogError($"AlternativeFuta: Skipping perineum pin {entry.Key}->{entry.Value}, out of range (gen mesh: {genVertCount} verts, target mesh: {targetVertCount} verts)."); continue; } var pair = new DAZMeshGraftVertexPair(); pair.vertexNum = entry.Key; pair.graftToVertexNum = entry.Value; graftPairs.Add(pair); } genMesh.meshGraft.vertexPairs = graftPairs.ToArray(); var vagMesh = mergedMesh.graftMesh; var vaginaPins = hideFemaleGens.val ? FemaleGenPins : FemaleGenPinsShowVagina; List graftPairs2 = new List(); foreach(var pair in vagMesh.meshGraft.vertexPairs) { graftPairs2.Add(pair); } int vagVertCount = vagMesh.baseVertices.Length; foreach(var entry in vaginaPins) { if(entry.Key < 0 || entry.Key >= vagVertCount || entry.Value < 0 || entry.Value >= targetVertCount) { SuperController.LogError($"AlternativeFuta: Skipping vagina pin {entry.Key}->{entry.Value}, out of range (vag mesh: {vagVertCount} verts, target mesh: {targetVertCount} verts)."); continue; } var pair = new DAZMeshGraftVertexPair(); pair.vertexNum = entry.Key; pair.graftToVertexNum = entry.Value; graftPairs2.Add(pair); } vagMesh.meshGraft.vertexPairs = graftPairs2.ToArray(); // Init gen mesh // Adding more hidden polys changes the UVs and breaks clothing genMesh.meshGraft.hiddenPolys = new int[0]; genMesh.graftTo = mergedMesh.targetMesh; genMesh.Init(); // Remove penis vertices as needed DAZMeshData penisMeshData = ScriptableObject.CreateInstance(typeof(DAZMeshData)) as DAZMeshData; penisMeshData.baseVertices = (Vector3[])genMesh.baseVertices.Clone(); penisMeshData.basePolyList = (MeshPoly[])genMesh.basePolyList.Clone(); penisMeshData.baseVerticesToUVVertices = genMesh.baseVerticesToUVVertices; penisMeshData.UVVertices = (Vector3[])genMesh.UVVertices.Clone(); penisMeshData.UVPolyList = (MeshPoly[])genMesh.UVPolyList.Clone(); penisMeshData.OrigUV = genMesh.OrigUV; if(!hideFemaleGens.val) { for(int i = 0; i < penisMeshData.UVPolyList.Length; i++) { var meshPoly = penisMeshData.UVPolyList[i]; foreach(int vert in meshPoly.vertices) { if(HidePerineumVerts.Contains(vert)) { meshPoly.materialNum = 2; break; } } } } genMesh.meshData = penisMeshData; // Remove vagina vertices as needed DAZMeshData vagMeshData = ScriptableObject.CreateInstance(typeof(DAZMeshData)) as DAZMeshData; vagMeshData.baseVertices = (Vector3[])vagMesh.baseVertices.Clone(); vagMeshData.basePolyList = (MeshPoly[])vagMesh.basePolyList.Clone(); vagMeshData.baseVerticesToUVVertices = vagMesh.baseVerticesToUVVertices; vagMeshData.UVVertices = (Vector3[])vagMesh.UVVertices.Clone(); vagMeshData.UVPolyList = (MeshPoly[])vagMesh.UVPolyList.Clone(); vagMeshData.OrigUV = (Vector2[])vagMesh.OrigUV.Clone(); for (int i = 0; i < vagMeshData.UVPolyList.Length; i++) { bool shouldAdd = !hideFemaleGens.val; // All verts on by default if showing vagina var meshPoly = vagMeshData.UVPolyList[i]; foreach(int vert in meshPoly.vertices) { if(!hideFemaleGens.val && HideFemalePelvisVertsWhenShowingVagina.Contains(vert)) { meshPoly.materialNum = 3; break; } if(VisibleFemaleGenVerts.Contains(vert)) { shouldAdd = true; break; } } if(!shouldAdd){ meshPoly.materialNum = 3; } } vagMesh.meshData = vagMeshData; // Remove the hip vertices // Typically this would be done by the hiddenPolys of the male gen graft, but touching that seems to break clothing... DAZMeshData baseMeshData = ScriptableObject.CreateInstance(typeof(DAZMeshData)) as DAZMeshData; baseMeshData.baseVertices = mergedMesh.targetMesh.baseVertices; baseMeshData.basePolyList = (MeshPoly[])mergedMesh.targetMesh.basePolyList.Clone(); baseMeshData.baseVerticesToUVVertices = mergedMesh.targetMesh.baseVerticesToUVVertices; baseMeshData.UVVertices = mergedMesh.targetMesh.UVVertices; baseMeshData.UVPolyList = (MeshPoly[])mergedMesh.targetMesh.UVPolyList.Clone(); baseMeshData.OrigUV = mergedMesh.targetMesh.OrigUV; for(int i = 0; i < baseMeshData.UVPolyList.Length; i++) { var meshPoly = baseMeshData.UVPolyList[i]; if(PelvisPolysTopRow.Contains(i)) { meshPoly.materialNum = 31; } } mergedMesh.targetMesh.meshData = baseMeshData; } else { genMesh.meshGraft.vertexPairs = new DAZMeshGraftVertexPair[0]; genMesh.meshGraft.hiddenPolys = new int[0]; genMesh.graftTo = mergedMesh.targetMesh; genMesh.Init(); } // Merge the skin! // This should result in the mergedMesh copying all of our gen mesh's vertices. mergedMesh.Merge(); // DAZSkinV2 also has a merge concept, which copies over the bone weights that determine how the mesh is skinned to the skeleton. // For this we'll use the male skin we created earlier and copy its verts DAZSkinV2 maleGenSkin = null; foreach(var skinv2 in maleAsset.GetComponentsInChildren()) { if(skinv2.dazMesh == maleMergedMesh.graftMesh) { maleGenSkin = skinv2; } } DAZSkinV2Wrapper genSkinv2 = mergedMesh.gameObject.AddComponent(); genSkinv2.Copy(maleGenSkin); // Merge the skins. skin.Merge(); skin.Init(); // Adjust the male auto colliders int mergedVertCount = mergedMesh.baseVertices.Length; foreach(var entry in maleAutoColliderConfigs) { AutoCollider ac = entry.Key; AutoColliderConfig config = entry.Value; if(ac.targetVertex >=0) ac.targetVertex = ValidateGraftVertex(config.targetVertex + mergedMesh.numGraftBaseVertices, mergedVertCount, ac.name, "targetVertex"); if(ac.anchorVertex1 >=0) ac.anchorVertex1 = ValidateGraftVertex(config.anchorVertex1 + mergedMesh.numGraftBaseVertices, mergedVertCount, ac.name, "anchorVertex1"); if(ac.anchorVertex2 >=0) ac.anchorVertex2 = ValidateGraftVertex(config.anchorVertex2 + mergedMesh.numGraftBaseVertices, mergedVertCount, ac.name, "anchorVertex2"); if(ac.oppositeVertex >=0) ac.oppositeVertex = ValidateGraftVertex(config.oppositeVertex + mergedMesh.numGraftBaseVertices, mergedVertCount, ac.name, "oppositeVertex"); } // Set the materials Material[] originalMaterials = skin.GPUmaterials; skin.CopyMaterials(); for (int i = 0; i < skin.dazMesh.materials.Length; i++) { skin.materialsEnabled[i] = true; skin.dazMesh.materialsEnabled[i] = true; if(i < originalMaterials.Length) { skin.GPUmaterials[i] = originalMaterials[i]; } else { Material mat = new Material(skin.GPUmaterials[0]); if (mat.HasProperty("_AlphaAdjust")) { mat.SetFloat("_AlphaAdjust", -1f); } skin.GPUmaterials[i] = mat; } } // Female gens if(useVaginaAsPelvisBase) { skin.GPUmaterials[31] = new Material(skin.GPUmaterials[28]); } else { skin.GPUmaterials[31] = new Material(skin.GPUmaterials[20]); } skin.FlushBuffers(); } private int ValidateGraftVertex(int index, int vertexCount, string colliderName, string fieldName) { if(index >= 0 && index < vertexCount) return index; SuperController.LogError($"AlternativeFuta: AutoCollider '{colliderName}' {fieldName} index {index} is out of range (merged mesh has {vertexCount} verts); disabling this anchor."); return -1; } private IEnumerator WaitForLoad() { // Wait for it do its thing... while (newCharacterContainer == null || newCharacter == null || !newCharacter.ready) { yield return null; } while (skin == null || !skin.wasInit) { skin = newCharacterContainer.GetComponentInChildren(); yield return null; } DAZMergedSkinV2 maleMergedSkin = maleCharacterContainer.GetComponentInChildren(true); while (maleMergedSkin == null) { maleMergedSkin = maleCharacterContainer.GetComponentInChildren(true); yield return null; } try { try { // There are 3 morph banks for the (up to) 3 grafted meshes. // We want our 2nd graft (morphBank3) to use the morph's for // The male's 1st graft (maleMorphBank2) characterRun.morphBank3 = characterSelector.maleMorphBank2; // Morph banks will try to apply morphs to connected meshes // This might be set if the model was previously male. Need to unset it characterRun.morphBank3.connectedMesh = null; characterRun.ResetMorphs(); characterSelector.maleMorphBank2.gameObject.SetActive(true); SetMaleTransformsActive(true); SetUpMorphs(); var useMaleMorphsOnFemale = characterSelector.GetBoolJSONParam("useMaleMorphsOnFemale"); if (useMaleMorphsOnFemale != null && !useMaleMorphsOnFemale.val) { foreach (var morph in characterSelector.maleMorphBank1.morphs) { morph.morphValue = 0f; } useMaleMorphsOnFemale.val = true; } } catch (Exception e) { SuperController.LogError($"Error setting up positioning morphs. {e}"); } // Penis skin.GPUmaterials[30] = new Material(maleMergedSkin.GPUmaterials[29]); if (useTransparentShader.val) { skin.GPUmaterials[30].shader = Shader.Find("Custom/Subsurface/TransparentGlossNMSeparateAlphaComputeBuff"); } else { skin.GPUmaterials[30].shader = skin.GPUmaterials[0].shader; } if (skin.GPUmaterials[30].HasProperty("_Tess") && skin.GPUmaterials[0].HasProperty("_Tess")) { skin.GPUmaterials[30].SetFloat("_Tess", skin.GPUmaterials[0].GetFloat("_Tess")); } if (skin.GPUmaterials[30].HasProperty("_TessPhong") && skin.GPUmaterials[0].HasProperty("_TessPhong")) { skin.GPUmaterials[30].SetFloat("_TessPhong", skin.GPUmaterials[0].GetFloat("_TessPhong")); } // Pelvis skin.GPUmaterials[29] = new Material(maleMergedSkin.GPUmaterials[28]); if (useTransparentShader.val) { skin.GPUmaterials[29].shader = Shader.Find("Custom/Subsurface/TransparentGlossNMSeparateAlphaComputeBuff"); } else { skin.GPUmaterials[29].shader = skin.GPUmaterials[0].shader; } if (skin.GPUmaterials[29].HasProperty("_Tess") && skin.GPUmaterials[0].HasProperty("_Tess")) { skin.GPUmaterials[29].SetFloat("_Tess", skin.GPUmaterials[0].GetFloat("_Tess")); } if (skin.GPUmaterials[29].HasProperty("_TessPhong") && skin.GPUmaterials[0].HasProperty("_TessPhong")) { skin.GPUmaterials[29].SetFloat("_TessPhong", skin.GPUmaterials[0].GetFloat("_TessPhong")); } // Create the material options var defaultsMat = skin.GPUmaterials[29]; if (useTransparentShader.val) { defaultsMat.shader = Shader.Find("Custom/Subsurface/TransparentGlossNMSeparateAlphaComputeBuff"); } genMaterialOptions = CreateMaterialOptions(new int[]{29,30}, defaultsMat); genMaterialOptions.overrideId = $"{this.storeId}PenisMaterials"; containingAtom.RegisterAdditionalStorable(genMaterialOptions); RestoreSavedMaterialOptions(); SyncAutoColliderSettings(); } catch (Exception e) { SuperController.LogMessage($"{e}"); } // Re-enable the male portions of the model skinLoaded = true; status.val = "ready"; // Pelvis if (showMaterial29 != null) { skin.materialsEnabled[29] = showMaterial29.val; } // Penis if (showMaterial30 != null) { showHideGenital(showMaterial30.val, false); // Second bool is 'norestore' to prevent application during loading } // Clear hidden-penis morph residue from older versions (they relied on // an unconditional restore-on-load that also acted as a cleanup) CleanupStaleHiddenMorphs(); // Migrate the old "Female Pelvis Out/In" default: saved scenes carry // 1.0 from the old default, which never actually applied back then. if (pelvisMorph2Value != null && Mathf.Abs(pelvisMorph2Value.val - 1f) < 0.001f) { pelvisMorph2Value.val = 0f; } // Apply testes collision setting if (disableTestesCollision != null) { UpdateTestesCollision(disableTestesCollision.val); } // There's a race condition here that prevents the autocollider update from taking effect, but I'm not sure what it is. Waiting for a few frames seems to be enough. for (int i = 0; i < 10; i++) { yield return new WaitForSecondsRealtime(0.5f); UpdateAutoColliders(); } } private void InitMainUI() { createStaticDescriptionTextMain("Alternative Futa", "Alternative Futa", false, 34); createStaticDescriptionTextMain("Version", "" + " " + date + "", true, 34); UIDynamic spacer = CreateSpacer(false); spacer.height = 40; UIDynamic spacer2 = CreateSpacer(true); spacer2.height = 40; // Left UI side createStaticDescriptionText("Main","Main",false,34); UIDynamicButton genMaterialOptionsButton = CreateButton("Penis Material Options", false); genMaterialOptionsButton.button.onClick.AddListener(() => { ShowMaterialOptionsUI(genMaterialOptionsUI); }); Skynet.ToolTipManager.AddTooltip(genMaterialOptionsButton, "Add/remove penis textures, edit coloring, gloss/specular/illumination properties, and hide penis materials."); seamColorBlendEnabled = new JSONStorableBool("Seam Color Blend", false, (bool val) => { RequestSeamRebake(); }); RegisterBool(seamColorBlendEnabled); var seamColorBlendToggle = CreateToggle(seamColorBlendEnabled, false); Skynet.ToolTipManager.AddTooltip(seamColorBlendToggle, "Matches the penis skin color to the body, then bakes a color-blend decal that gradually fades the graft into the body on both sides of the connection seam, hiding color/texture differences. Works with both the opaque and transparent shaders. Reloading the skin (also triggered by the hide/shader toggles) clears the blend and unchecks this; re-enable it afterwards."); // Temporary calibration sliders for the seam blend seamPatchRadius = new JSONStorableFloat("Seam Patch Radius", SeamColorBlendRadius, (float val) => { RequestSeamRebake(); }, 0.02f, 0.2f, true); RegisterFloat(seamPatchRadius); var seamPatchSlider = CreateSlider(seamPatchRadius, false); Skynet.ToolTipManager.AddTooltip(seamPatchSlider, "(Pelvis visible mode) Size of the soft color patch that straddles the pelvis seam on both sides. 0.1 is the original value."); seamFadeRange = new JSONStorableFloat("Seam Fade Range", HiddenSeamFadeRadius, (float val) => { RequestSeamRebake(); }, 0.01f, 0.1f, true); RegisterFloat(seamFadeRange); var seamFadeSlider = CreateSlider(seamFadeRange, false); Skynet.ToolTipManager.AddTooltip(seamFadeSlider, "(Hide Male Pelvis only) How far up the shaft the body-color fade reaches before the penis texture takes over."); seamAnchorOffset = new JSONStorableFloat("Seam Anchor Offset", 0f, (float val) => { RequestSeamRebake(); }, -0.05f, 0.05f, true); RegisterFloat(seamAnchorOffset); var seamAnchorSlider = CreateSlider(seamAnchorOffset, false); Skynet.ToolTipManager.AddTooltip(seamAnchorSlider, "(Hide Male Pelvis only) Shifts the fade's starting line: negative moves it into the body, positive moves it up the shaft."); // Default 0.95: even with coverage-weighted, linear-space averaging the // painted flat color renders slightly brighter than the surrounding // textured skin (lighting-driven residual that texture-space math // cannot capture). 0.95 was measured against real skin in the scene. seamColorBrightness = new JSONStorableFloat("Seam Color Brightness", 0.95f, (float val) => { RequestSeamRebake(); }, 0.3f, 1.5f, true); RegisterFloat(seamColorBrightness); var seamBrightnessSlider = CreateSlider(seamColorBrightness, false); Skynet.ToolTipManager.AddTooltip(seamBrightnessSlider, "Darkens or brightens the painted blend color (both modes). The painted flat color tends to render slightly brighter than real textured skin; 0.95 compensates for that."); seamSurfaceRange = new JSONStorableFloat("Seam Surface Range", 0.015f, (float val) => { RequestSeamRebake(); }, 0.005f, 0.2f, true); RegisterFloat(seamSurfaceRange); var seamSurfaceSlider = CreateSlider(seamSurfaceRange, false); Skynet.ToolTipManager.AddTooltip(seamSurfaceSlider, "(Hide Male Pelvis only) How far the specular/gloss/normal map detail is faded out around the contact area. Keep this narrow: a wide fade looks worse than the seam itself."); UIDynamicButton reload = CreateButton("Reload Skin", false); reload.button.onClick.AddListener(() => { ReloadSkin(); }); Skynet.ToolTipManager.AddTooltip(reload, "Reload the skin textures. This function is triggerable."); UIDynamic spacer12 = CreateSpacer(false); spacer12.height = 0; createStaticDescriptionText("Options","Options",false,34); // Pelvis showMaterial29 = new JSONStorableBool("Show Pelvis Material", true, (val) => { if (skin != null) { skin.materialsEnabled[29] = val; } }); RegisterBool(showMaterial29); //CreateToggle(showMaterial29, true); // Penis showMaterial30 = new JSONStorableBool("Hide Penis", false, (bool val) => { showHideGenital(val, false); }); RegisterBool(showMaterial30); var hidePenisToggle = CreateToggle(showMaterial30, false); Skynet.ToolTipManager.AddTooltip(hidePenisToggle, "Hides the penis and testicles using morphs, to allow wearing of clothing. Use the 'Hidden Genital Reduction' slider to adjust. If the Realistic Foreskin & Erection Simulation plugins are loaded, this function is controlled remotely."); hiddenPenisLengthValue = AddSlider("Hidden Penis Reduction", 1f, 0f, 5f, out hiddenPenisLengthSlider, (float value) => { if (penisLengthMorph != null) { hiddenPenisLengthValue.val = value; showHideGenital(showMaterial30.val, true); } }, rightSide: false ); Skynet.ToolTipManager.AddTooltip(hiddenPenisLengthSlider, "Define the hidden genital morph value multiplier for penis length and scrotum size. Activate 'Hide Penis' and use the slider to find the perfect spot."); disableTestesCollision = new JSONStorableBool("Disable Testes Collision", false); RegisterBool(disableTestesCollision); var disableTestesCollisionToggle = CreateToggle(disableTestesCollision, false); disableTestesCollisionToggle.toggle.onValueChanged.AddListener((val) => { UpdateTestesCollision(val); }); Skynet.ToolTipManager.AddTooltip(disableTestesCollisionToggle, "Disables collision detection for the testes to prevent unwanted physics interactions."); // Sliders UIDynamic spacer11 = CreateSpacer(false); spacer11.height = 0; createStaticDescriptionText("Morphs","Morphs",false,34); genYOffsetValue = AddSlider("Bone+Mesh Y Offset", genBoneYOffsetDefault, -0.1f, 0.1f, out genYOffsetValueSlider, (float value) => { if(genYOffsetMorph != null){ genYOffsetMorph.morphValue = value; } }); Skynet.ToolTipManager.AddTooltip(genYOffsetValueSlider, "Morph to move the male pelvis to make it fit the rest of the hip/pelvis area."); genZOffsetValue = AddSlider("Bone+Mesh Z Offset", genBoneZOffsetDefault, -0.1f, 0.1f, out genZOffsetValueSlider, (float value) => { if(genZOffsetMorph != null){ genZOffsetMorph.morphValue = value; } }); Skynet.ToolTipManager.AddTooltip(genZOffsetValueSlider, "Morph to move the male pelvis to make it fit the rest of the hip/pelvis area."); hidePerineumOffsetValue = AddSlider("Hide Perimeum", hidePerineumDefault, -1f, 1f, out hidePerineumOffsetValueSlider, (float value) => { if(hidePerineumMorph != null){ hidePerineumMorph.morphValue = value; } }); Skynet.ToolTipManager.AddTooltip(hidePerineumOffsetValueSlider, "Morph to hide the part of the perimeum covering the vagina. Use this (default 1.0) if you want to expose the vagina."); gapFixOffsetValue = AddSlider("Gap Fix", 0, 0f, 1f, out gapFixOffsetValueSlider, (float value) => { if (gapFixMorph != null) { gapFixMorph.morphValue = value; } }, rightSide: false ); Skynet.ToolTipManager.AddTooltip(gapFixOffsetValueSlider, "Morph created by Stopper by combining a few built-in morphs to help tighten the gen area to hide gaps."); // Right UI side createStaticDescriptionText("Presets","Presets",true,34); var savePresetUI = CreateButton("Save Preset", true); savePresetUI.button.onClick.AddListener(() => { savePreset(); }); SetupButtonWithLayout(savePresetUI, 259f); Skynet.ToolTipManager.AddTooltip(savePresetUI, "Save a preset for Alternatvie Futa, including skin textures, morphs and options."); var loadPresetUI = CreateButton("Load Preset", true); loadPresetUI.button.onClick.AddListener(() => { loadPreset(); }); SetupButtonWithoutLayout(loadPresetUI, 259f, new Vector2(278, -188)); Skynet.ToolTipManager.AddTooltip(loadPresetUI, "Load a preset for Alternative Futa by choosing the preset file.\n\nIf you want to trigger preset loading with a preset file path, e.g. in timeline or from a UIButton, use 'Receiver' = 'plugin#?_Stopper.AlternativeFuta' (adjust ? to match the plugin number) and 'Receiver Target' = 'Load Preset From Path'. Enter a path formatted 'Saves\\AltFuta\\{filename}.altfuta'."); mergeLoad = new JSONStorableBool("Merge Load", false); RegisterBool(mergeLoad); var mergeLoadToggle = CreateToggle(mergeLoad, true); SetupToggleWithoutLayout(mergeLoadToggle, 256f, new Vector2(277, -250)); Skynet.ToolTipManager.AddTooltip(mergeLoadToggle, "Merge loading a preset will add the new properties without deleting existing ones."); createStaticDescriptionText("","",true,114); hideFemaleGens = new JSONStorableBool("Hide Female Gens", true); RegisterBool(hideFemaleGens); var hideGensToggle = CreateToggle(hideFemaleGens, true); hideGensToggle.toggle.onValueChanged.AddListener((val) => { ReloadSkin(); }); Skynet.ToolTipManager.AddTooltip(hideGensToggle, "Include or exclude a vagina. If excluded, a base G2F mesh is used which does not have an anus. To make an altfuta with vagina, deactivate this toggle, adjust 'Hide Perimeum' (default 1.0) and 'Hide Vagina' (default 0) sliders and load corresponding female gens textures (instead of altfuta torso)."); var vaginaHideDefault = hideFemaleGens.val? 1.0f : 0f; vaginaHideOffsetValue = AddSlider("Hide Vagina", vaginaHideDefault, 0f, 2f, out vaginaHideOffsetValueSlider, (float value) => { if(vaginaHideMorph != null){ vaginaHideMorph.morphValue = value; } }); Skynet.ToolTipManager.AddTooltip(vaginaHideOffsetValueSlider, "Morph to move the vagina back and forth to position it behind the penis."); hideMalePelvis = new JSONStorableBool("Hide Male Pelvis", false); RegisterBool(hideMalePelvis); var hideMalePelvisToggle = CreateToggle(hideMalePelvis, true); hideMalePelvisToggle.toggle.onValueChanged.AddListener((val) => { ReloadSkin(); }); Skynet.ToolTipManager.AddTooltip(hideMalePelvisToggle, "Hides the pelvis portion of male gen mesh. Activate this (while 'Hide female gens' is deactivated) to get a completely unobstructed vagina, and use the 'Hide Perimeum' morph. Be aware: 'Hide Male Pelvis' will un-merge the male genital from the pelvis."); useTransparentShader = new JSONStorableBool("Use Transparent Shader", false); RegisterBool(useTransparentShader); var transparentShaderToggle = CreateToggle(useTransparentShader, true); transparentShaderToggle.toggle.onValueChanged.AddListener((val) => { ReloadSkin(); }); Skynet.ToolTipManager.AddTooltip(transparentShaderToggle, "Use a transparent shader for the male gens. Allows for an alpha mask and blending, but doesn't support tesselation."); createStaticDescriptionText("","",true,120); genYMeshOffsetValue = AddSlider("Mesh Y Offset", 0f, -0.1f, 0.1f, out genYMeshOffsetValueSlider, (float value) => { if (genYMeshOffsetMorph != null) { genYMeshOffsetMorph.morphValue = value; } }, rightSide: true ); Skynet.ToolTipManager.AddTooltip(genYMeshOffsetValueSlider, "Morph to move the male pelvis to make it fit the rest of the hip/pelvis area."); genZMeshOffsetValue = AddSlider("Mesh Z Offset", 0f, -0.1f, 0.1f, out genZMeshOffsetValueSlider, (float value) => { if (genZMeshOffsetMorph != null) { genZMeshOffsetMorph.morphValue = value; } }, rightSide: true ); Skynet.ToolTipManager.AddTooltip(genZMeshOffsetValueSlider, "Morph to move the male pelvis to make it fit the rest of the hip/pelvis area."); // Default was 1.0 for years but never actually applied (an unconditional // restore zeroed the morph on every load); now that slider values stick, // 1.0 would bulge the hidden female pelvis out at the penis base. pelvisMorph2Value = AddSlider("Female Pelvis Out/In", 0f, 0f, 3f, out pelvisMorph2ValueSlider, (float value) => { if (pelvisMorph2 != null) { pelvisMorph2.morphValue = value; } }, rightSide: true ); Skynet.ToolTipManager.AddTooltip(pelvisMorph2ValueSlider, "Move the invisible female pelvis (which sits behind the visible male pelvis) in and out, to allow use of female pubic hair. Morph: 'AAX Genitals Fat' by AshAuryn."); pelvisMorph1Value = AddSlider("Female Pelvis Up/Down", 0f, 0f, 2f, out pelvisMorph1ValueSlider, (float value) => { if (pelvisMorph1 != null) { pelvisMorph1.morphValue = value; } }, rightSide: true ); Skynet.ToolTipManager.AddTooltip(pelvisMorph1ValueSlider, "Move the invisible female pelvis (which sits behind the visible male pelvis) up and down, to position female pubic hair. Be aware: This will also stretch the vagina (if activated). Morph: 'Pubis_Size_Z' by VL_13."); pelvisMorph3Value = AddSlider("Female Pelvis Wide/Narrow", 0f, -0.3f, 2f, out pelvisMorph3ValueSlider, (float value) => { if (pelvisMorph3 != null) { pelvisMorph3.morphValue = value; } }, rightSide: true ); Skynet.ToolTipManager.AddTooltip(pelvisMorph3ValueSlider, "Stretch the invisible female pelvis (which sits behind the visible male pelvis) to adjust female pubic hair. Morph: 'AAX Pubic Area Press' by AshAuryn."); HideMaterialOptionsIUI(); } // One-time cleanup for morph values left behind by the old capture bug: // if 'Hide Penis' is off but the character still holds the exact // hidden-penis override signature (MonsPubis 4.0 + Genital Bulge 2.0 + // Genital Area 0.5), those values are stale leftovers — clear them. // They create a body-colored mound at the crotch that is normally hidden // behind the male pelvis mesh and only shows up with 'Hide Male Pelvis'. private void CleanupStaleHiddenMorphs() { if (showMaterial30 != null && showMaterial30.val) return; if (hiddenMorphsApplied != null && hiddenMorphsApplied.val) return; bool monsStale = monsPubicInOutMorph != null && Mathf.Abs(monsPubicInOutMorph.morphValue - 4.0f) < 0.01f; bool bulgeStale = genitalBulge1Morph != null && Mathf.Abs(genitalBulge1Morph.morphValue - 2.0f) < 0.01f; bool areaStale = genitalAreaInOutMorph != null && Mathf.Abs(genitalAreaInOutMorph.morphValue - 0.5f) < 0.01f; int staleCount = (monsStale ? 1 : 0) + (bulgeStale ? 1 : 0) + (areaStale ? 1 : 0); // Two or more of the override constants together are effectively // impossible as deliberate user values; treat as stale and clear all. if (staleCount < 2) return; if (monsPubicInOutMorph != null) monsPubicInOutMorph.morphValue = 0f; if (genitalBulge1Morph != null) genitalBulge1Morph.morphValue = 0f; if (genitalAreaInOutMorph != null) genitalAreaInOutMorph.morphValue = 0f; if (penisLengthMorph != null && penisLengthMorph.morphValue < 0f) penisLengthMorph.morphValue = 0f; if (scCastrateMorph != null && scCastrateMorph.morphValue > 0f) scCastrateMorph.morphValue = 0f; if (characterRun != null) characterRun.SetNeedsAutoColliderUpdate(); SuperController.LogMessage("AlternativeFuta: cleared stale hidden-penis morph values left behind by an older version."); } // Morph manipulation to hide the penis (instead of hiding the material, which leads to a black hole) private void showHideGenital(bool val, bool norestore) { //SuperController.LogMessage($"showHideGenital: isLoaded={isLoaded}, hide={val}, norestore={norestore}"); //ZZZ if (isLoaded == false) return; if (val) { // Save existing morph values & testicle collision setting. // Skipped while the override is already applied: capturing then would // store the hidden values themselves and lose the user's originals // (e.g. on skin reloads while 'Hide Penis' is on). if (norestore == false && hiddenMorphsApplied.val == false) { //SuperController.LogMessage($"showHideGenital: SAVING restore values...}"); //ZZZ if (genZMeshOffsetMorph != null) genZMeshOffsetValue_restore.val = genZMeshOffsetValue.val; //if (genYMeshOffsetValue != null) genYMeshOffsetValue_restore.val = genYMeshOffsetValue.val; if (penisLengthMorph != null) penisLengthMorph_restore.val = penisLengthMorph.morphValue; if (genitalBulge1Morph != null) genitalBulge1Morph_restore.val = genitalBulge1Morph.morphValue; if (scCastrateMorph != null) scCastrateMorph_restore.val = scCastrateMorph.morphValue; if (genitalAreaInOutMorph != null) genitalAreaInOutMorph_restore.val = genitalAreaInOutMorph.morphValue; if (monsPubicInOutMorph != null) monsPubicInOutMorph_restore.val = monsPubicInOutMorph.morphValue; if (pelvisMorph1 != null) pelvisMorph1_restore.val = pelvisMorph1.morphValue; if (pelvisMorph2 != null) pelvisMorph2_restore.val = pelvisMorph2.morphValue; if (pelvisMorph3 != null) pelvisMorph3_restore.val = pelvisMorph3.morphValue; if (disableTestesCollision != null) disableTestesCollision_restore.val = disableTestesCollision.val; } // Set new (hidden) morph values //SuperController.LogMessage($"showHideGenital: SETTING fixed hidden values...}"); //ZZZ if (genZMeshOffsetMorph != null) genZMeshOffsetValue.val = -0.005f; //if (genYMeshOffsetValue != null) genYMeshOffsetValue.val = 0.022f; if (penisLengthMorph != null) penisLengthMorph.morphValue = -2.8f * hiddenPenisLengthValue.val; if (genitalBulge1Morph != null) genitalBulge1Morph.morphValue = 2.0f; if (scCastrateMorph != null) scCastrateMorph.morphValue = 1.5f * hiddenPenisLengthValue.val; if (genitalAreaInOutMorph != null) genitalAreaInOutMorph.morphValue = 0.5f; if (monsPubicInOutMorph != null) monsPubicInOutMorph.morphValue = 4.0f; if (pelvisMorph1 != null) pelvisMorph1.morphValue = 0f; if (pelvisMorph2 != null) pelvisMorph2.morphValue = 0f; if (pelvisMorph3 != null) pelvisMorph3.morphValue = 0f; hiddenMorphsApplied.val = true; // Disable testes collision UpdateTestesCollision(false); } else if (hiddenMorphsApplied.val) { //SuperController.LogMessage($"showHideGenital: RE-SETTING to restore values...}"); //ZZZ // Reset user values of morphs and testicle collision if (genZMeshOffsetMorph != null) genZMeshOffsetValue.val = genZMeshOffsetValue_restore.val; //if (genYMeshOffsetValue != null) genYMeshOffsetValue.val = genYMeshOffsetValue_restore.val; if (penisLengthMorph != null) penisLengthMorph.morphValue = penisLengthMorph_restore.val; if (genitalBulge1Morph != null) genitalBulge1Morph.morphValue = genitalBulge1Morph_restore.val; if (scCastrateMorph != null) scCastrateMorph.morphValue = scCastrateMorph_restore.val; if (genitalAreaInOutMorph != null) genitalAreaInOutMorph.morphValue = genitalAreaInOutMorph_restore.val; if (monsPubicInOutMorph != null) monsPubicInOutMorph.morphValue = monsPubicInOutMorph_restore.val; if (pelvisMorph1 != null) pelvisMorph1.morphValue = pelvisMorph1_restore.val; if (pelvisMorph2 != null) pelvisMorph2.morphValue = pelvisMorph2_restore.val; if (pelvisMorph3 != null) pelvisMorph3.morphValue = pelvisMorph3_restore.val; hiddenMorphsApplied.val = false; // Reset testes collision UpdateTestesCollision(disableTestesCollision_restore.val); } // Refresh morphs if (characterRun != null) { characterRun.SetNeedsAutoColliderUpdate(); } // Move (female) pubic hair a little bit out to male pelvis position /* foreach (DAZSkinWrap wrap in containingAtom.GetComponentsInChildren(true)) { if (!wrap.draw) continue; if (wrap.wrapName != "Normal") continue; if (wrap.transform.name != "PantyRegionScalp") continue; Transform hairItem = wrap.transform.parent; if (!hairItem.gameObject.activeInHierarchy) continue; if (hairItem == null || hairItem.parent == null || hairItem.parent.name != "CustomHairItem") continue; SuperController.LogMessage($"Moving pubic hair: {hairItem.name}"); wrap.surfaceOffset = 0.005f; wrap.moveToSurfaceOffset = 0.0f; }*/ } private void loadPreset() { checkUVsetName(); FileManagerSecure.CreateDirectory(altFutaSavePath); var shortcuts = FileManagerSecure.GetShortCutsForDirectory(altFutaSavePath); SuperController.singleton.GetMediaPathDialog(LoadPreset, altFutaFileExt, altFutaSavePath, false, true, false, null, false, shortcuts); } private void loadPresetWithPath(string path) //RunRudolf, loading a preset file directly from a given path { if (string.IsNullOrEmpty(path)) return; checkUVsetName(); LoadPreset(path); } private void savePreset() { FileManagerSecure.CreateDirectory(altFutaSavePath); var fileBrowserUI = SuperController.singleton.fileBrowserUI; fileBrowserUI.SetTitle("Save AltFuta Preset"); fileBrowserUI.fileRemovePrefix = null; fileBrowserUI.hideExtension = false; fileBrowserUI.keepOpen = false; fileBrowserUI.fileFormat = altFutaFileExt; fileBrowserUI.defaultPath = altFutaSavePath; fileBrowserUI.showDirs = true; fileBrowserUI.shortCuts = null; fileBrowserUI.browseVarFilesAsDirectories = false; fileBrowserUI.SetTextEntry(true); fileBrowserUI.Show(SavePreset); fileBrowserUI.fileEntryField.text = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds + "." + altFutaFileExt; fileBrowserUI.ActivateFileNameField(); } private IEnumerator InitMaterialUI() { // The material selction prefab doesn't do well with a layout, so we're just going to remove it. while(scriptUI == null) { if(this.UITransform != null) { scriptUI = this.UITransform.GetComponentInChildren(); } yield return null; } if(scriptUI == null) { SuperController.LogMessage("Unable to load script UI."); yield break; } UnityEngine.UI.VerticalLayoutGroup layout = scriptUI.fullWidthUIContent.gameObject.GetComponent(); Destroy(layout); // Find a pre-existing material option UI to use as a template. Any would likely do... while( materialUIPrefab == null) { var mouthMatTransform = containingAtom.transform.Find("UI/UIPlaceHolderModel/UIModel/Canvas/Panel/Content/Mouth Materials"); if(mouthMatTransform != null) { materialUIPrefab = mouthMatTransform.gameObject; } else { yield return null; } } // Wait for the skin to load while(skinLoaded == false) { yield return null; } try { if(genMaterialOptionsUI == null) { genMaterialOptionsUI = UnityEngine.Object.Instantiate(materialUIPrefab.transform); genMaterialOptionsUI.SetParent(scriptUI.fullWidthUIContent,false); } // Setting all box backgrounds transparent string[] paths = { "", "TabGroup/TabParams", "TabGroup/TabShader", "TabGroup/TabTextures", "TabGroup/TabParams/Background", "TabGroup/TabShader/Background", "TabGroup/TabTextures/Background" }; foreach (string path in paths) { Transform t; if (string.IsNullOrEmpty(path)) t = genMaterialOptionsUI; // Mouth Materials(Clone) else t = genMaterialOptionsUI.Find(path); if (t == null) { //SuperController.LogMessage($"Not found: {path}"); continue; } Image img = t.GetComponent(); img.color = Color.clear; } // Remove empty slots, shifting the rest up Transform t2 = genMaterialOptionsUI.Find("TabGroup/TabTextures/Texture4"); if (t2 != null) { t2.gameObject.SetActive(false); } Transform t4 = genMaterialOptionsUI.Find("TabGroup/TabTextures/Texture4"); Transform t5 = genMaterialOptionsUI.Find("TabGroup/TabTextures/Texture5"); Transform t6 = genMaterialOptionsUI.Find("TabGroup/TabTextures/Texture6"); t4.gameObject.SetActive(false); float offset = 170f; RectTransform rt5 = t5.GetComponent(); RectTransform rt6 = t6.GetComponent(); rt5.anchoredPosition += new Vector2(0f, offset); rt6.anchoredPosition += new Vector2(0f, offset); // Shifting the sliders up over missing no 7 Transform p8 = genMaterialOptionsUI.Find("TabGroup/TabParams/LeftSide/Param8Slider"); Transform p9 = genMaterialOptionsUI.Find("TabGroup/TabParams/LeftSide/Param9Slider"); Transform p10 = genMaterialOptionsUI.Find("TabGroup/TabParams/LeftSide/Param10Slider"); RectTransform rt8 = p8.GetComponent(); RectTransform rt9 = p9.GetComponent(); RectTransform rt10 = p10.GetComponent(); offset = 130f; rt8.anchoredPosition += new Vector2(0f, offset); rt9.anchoredPosition += new Vector2(0f, offset); rt10.anchoredPosition += new Vector2(0f, offset); // Activating UI element genMaterialOptionsUI.gameObject.SetActive(false); genMaterialOptions.SetUI(genMaterialOptionsUI); genMaterialOptions.SyncAllParameters(); genMaterialOptions.InitUI(); // Adding some space above everything to allow displaying mouse over paths of textures on the top RectTransform rt = genMaterialOptionsUI.GetComponent(); rt.anchoredPosition = new Vector2( rt.anchoredPosition.x, rt.anchoredPosition.y - 120f ); // Back button. Not the prettiest but it'll do Transform backButtonTransform = UnityEngine.Object.Instantiate(manager.configurableButtonPrefab.transform); backButtonTransform.SetParent(scriptUI.fullWidthUIContent,false); UIDynamicButton backButton = backButtonTransform.GetComponent(); backButton.label = "Back"; backButton.button.onClick.AddListener(() => { HideMaterialOptionsIUI(); }); RectTransform rect = backButtonTransform.gameObject.GetComponent(); rect.anchoredPosition = new Vector2( rect.anchoredPosition.x, rect.anchoredPosition.y + 10f ); rect.anchorMax = new Vector2(0.15f,1f); scriptUI.fullWidthUIContent.gameObject.SetActive(false); RestoreSavedMaterialOptions(); overrideFemalePelvisTextures = false; } catch (Exception e) { SuperController.LogError($"Error initializing material options ui. {e}"); } } private void SetUpMorphs() { var bank = characterSelector.maleMorphBank2; ZeroDuplicateMorphs(bank); genYOffsetMorph = bank.GetMorphByDisplayName("AltFuta Bone+Mesh Y-Offset"); if(genYOffsetMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Bone+Mesh Y-Offset'. Genitals may be positioned incorrectly."); } else { morphs.Add(genYOffsetMorph); } genZOffsetMorph = bank.GetMorphByDisplayName("AltFuta Bone+Mesh Z-Offset"); if(genZOffsetMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Bone+Mesh Z-Offset'. Genitals may be positioned incorrectly."); } else { morphs.Add(genZOffsetMorph); } genYMeshOffsetMorph = bank.GetMorphByDisplayName("AltFuta Mesh Y-Offset"); if(genYMeshOffsetMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Mesh Y-Offset'. Genitals may be positioned incorrectly."); } else { morphs.Add(genYMeshOffsetMorph); } genZMeshOffsetMorph = bank.GetMorphByDisplayName("AltFuta Mesh Z-Offset"); if(genZMeshOffsetMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Mesh Z-Offset'. Genitals may be positioned incorrectly."); } else { morphs.Add(genZMeshOffsetMorph); } hidePerineumMorph = bank.GetMorphByDisplayName("AltFuta Hide Perineum"); if(hidePerineumMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Hide Perineum'. Genitals may be positioned incorrectly."); } else { morphs.Add(hidePerineumMorph); } gapFixMorph = characterSelector.maleMorphBank1.GetMorphByDisplayName("AltFuta Gap Fix"); if(gapFixMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Gap Fix'. Genitals may be positioned incorrectly."); } else { morphs.Add(gapFixMorph); } vaginaHideMorph = characterSelector.femaleMorphBank2.GetMorphByDisplayName("AltFuta Vagina Hide"); if(vaginaHideMorph == null){ SuperController.LogError($"Unable to find morph 'AltFuta Vagina Hide'. Genitals may be positioned incorrectly."); } else { morphs.Add(vaginaHideMorph); } pelvisMorph1 = characterSelector.femaleMorphBank2.GetMorphByDisplayName("Pubis_Size_Z"); if (pelvisMorph1 == null) { pelvisMorph1 = characterSelector.femaleMorphBank2.GetMorphByDisplayName("Pubis Size Z"); } if(pelvisMorph1 == null){ SuperController.LogError($"Unable to find morph 'Pubis Size Z' by VL_13."); } else { morphs.Add(pelvisMorph1); } pelvisMorph2 = characterSelector.femaleMorphBank2.GetMorphByDisplayName("AAX Genitals Fat"); if(pelvisMorph2 == null){ SuperController.LogError($"Unable to find morph 'AAX Genitals Fat' by AshAuryn."); } else { morphs.Add(pelvisMorph2); } pelvisMorph3 = characterSelector.femaleMorphBank2.GetMorphByDisplayName("AAX Pubic Area Press"); if(pelvisMorph3 == null){ SuperController.LogError($"Unable to find morph 'AAX Pubic Area Press' by AshAuryn."); } else { morphs.Add(pelvisMorph3); } // Add AshAuryn.AshAuryn's_101_Female_Genitalia_Morphs.1:/Custom/Atom/Person/Morphs/female_genitalia/ASHAURYN OFFICIAL/TOOLS/MORPHS/GENITALIA SCULPTING/AAX Mons Lower Adjust 2.vmi? genYOffsetValue.setCallbackFunction(genYOffsetValue.val); genZOffsetValue.setCallbackFunction(genZOffsetValue.val); genYMeshOffsetValue.setCallbackFunction(genYMeshOffsetValue.val); genZMeshOffsetValue.setCallbackFunction(genZMeshOffsetValue.val); hidePerineumOffsetValue.setCallbackFunction(hidePerineumOffsetValue.val); gapFixOffsetValue.setCallbackFunction(gapFixOffsetValue.val); vaginaHideOffsetValue.setCallbackFunction(vaginaHideOffsetValue.val); pelvisMorph1Value.setCallbackFunction(pelvisMorph1Value.val); pelvisMorph2Value.setCallbackFunction(pelvisMorph2Value.val); pelvisMorph3Value.setCallbackFunction(pelvisMorph3Value.val); hiddenPenisLengthValue.setCallbackFunction(hiddenPenisLengthValue.val); // Initiate built-in morphs (RunRudolf) Selector = containingAtom.GetComponentInChildren(); penisLengthMorph = Selector.morphsControlMaleUI.GetMorphByUid("Penis Length"); genitalBulge1Morph = Selector.morphsControlMaleUI.GetMorphByUid("Genital Bulge 1"); scCastrateMorph = Selector.morphsControlMaleUI.GetMorphByUid("Sc_Castrate"); genitalAreaInOutMorph = Selector.morphsControlFemaleUI.GetMorphByUid("703-Genital-Area-Move-In.Out"); monsPubicInOutMorph = Selector.morphsControlFemaleUI.GetMorphByUid("704-MonsPubis-In.Out-a"); //flaccidErectMorph = Selector.morphsControlMaleUI.GetMorphByUid("Flaccid / Erect"); //flaccidErectAdjustMorph = Selector.morphsControlMaleUI.GetMorphByUid("Flaccid / Erect Adjust"); //mvrPenisBulgeMorph = Selector.morphsControlMaleUI.GetMorphByUid("MVR_PenisBulge"); //roundBulgeMorph = Selector.morphsControlMaleUI.GetMorphByUid("Round Bulge"); } // Adapted from Everlaster's TittyMagic v70 private void ZeroDuplicateMorphs(DAZMorphBank morphBank) { if(string.IsNullOrEmpty(packageId)) { return; } var versionRegex = new Regex(@"\.\d+$", RegexOptions.Compiled); string packageName = versionRegex.Split(packageId)[0]; foreach(var morph in morphBank.morphs) { if (morph.isInPackage && morph.morphValue != 0 && morph.packageUid.StartsWith(packageName) && morph.packageUid != packageId) { morph.morphValue = 0; } } } private void SetUpMeshMorph(DAZMorph morph, Vector3 direction) { morph.region = "Morph/Genitalia"; morph.min = -1f; morph.max = 1f; morph.visible = true; morph.isRuntime = true; morph.isDemandLoaded = false; // Vertex Deltas var deltas = new List(); for(int i = 0; i < genMesh.baseVertices.Length; i++) { deltas.Add(new DAZMorphVertex(){vertex = i, delta = direction}); } morph.deltas = deltas.ToArray(); morph.numDeltas = morph.deltas.Length; morph.formulas = new DAZMorphFormula[0]; } private void SetUpBoneMorph(DAZMorph morph, DAZMorphFormulaTargetType targetType, Vector3 direction) { SetUpMeshMorph(morph,direction); // Bone Morphs var boneFormulas = new List(); boneFormulas.Add(new DAZMorphFormula(){target = "Gen1", multiplier = 1f, targetType = targetType}); boneFormulas.Add(new DAZMorphFormula(){target = "Gen2", multiplier = 1f, targetType = targetType}); boneFormulas.Add(new DAZMorphFormula(){target = "Gen3", multiplier = 1f, targetType = targetType}); boneFormulas.Add(new DAZMorphFormula(){target = "Testes", multiplier = 1f, targetType = targetType}); morph.formulas = boneFormulas.ToArray(); } private DAZMorphFormula MorphBoneFormula(string boneName, DAZMorphFormulaTargetType targetType) { var formula = new DAZMorphFormula(); formula.targetType = targetType; formula.target = boneName; formula.multiplier = 1f; return formula; } private MaterialOptionsWrapper CreateMaterialOptions(int[] matIndexes, Material defaultMaterial) { var matOpts = newCharacterContainer.AddComponent(); matOpts.textureGroup1 = new MaterialOptionTextureGroup(); matOpts.SetCustomTextureFolder("Custom/Atom/Person/Textures"); matOpts.materialForDefaults = defaultMaterial; matOpts.paramMaterialSlots = matIndexes; matOpts.textureGroup1.materialSlots = matOpts.paramMaterialSlots; matOpts.skin = skin; matOpts.enabled = true; matOpts.SyncAllParameters(); return matOpts; } private void ShowMaterialOptionsUI(Transform matOptsUI) { if(matOptsUI == null) return; if(genMaterialOptionsUI != null && genMaterialOptionsUI != matOptsUI) genMaterialOptionsUI.gameObject.SetActive(false); scriptUI.leftUIContent.gameObject.SetActive(false); scriptUI.rightUIContent.gameObject.SetActive(false); scriptUI.fullWidthUIContent.gameObject.SetActive(true); matOptsUI.gameObject.SetActive(true); //Adjusting canvas height var le = scriptUI.fullWidthUIContent.GetComponent(); if (le == null) { le = scriptUI.fullWidthUIContent.gameObject.AddComponent(); } le.preferredHeight = 1450f; Canvas.ForceUpdateCanvases(); } private void HideMaterialOptionsIUI() { if(genMaterialOptionsUI != null) genMaterialOptionsUI.gameObject.SetActive(false); if(scriptUI != null) { //Adjusting canvas height var le = scriptUI.fullWidthUIContent.GetComponent(); if (le == null) { le = scriptUI.fullWidthUIContent.gameObject.AddComponent(); } le.preferredHeight = 10f; Canvas.ForceUpdateCanvases(); scriptUI.leftUIContent.gameObject.SetActive(true); scriptUI.rightUIContent.gameObject.SetActive(true); scriptUI.fullWidthUIContent.gameObject.SetActive(false); } } private void RestoreSavedMaterialOptions() { try { if (genMaterialOptions == null) { return; } // Restore the skin material options from the previous (non-futa) skin if (oldSkinOpts != null && newCharacter != null) { foreach (var json in oldSkinOpts){ foreach (var options in newCharacter.GetComponentsInChildren()) { string id = json["id"]; if (id != null && id == options.storeId) { options.RestoreFromJSON(new JSONClass()); options.RestoreFromJSON(json); } } } } var textureControl = newCharacter.GetComponentInChildren(); if(textureControl != null && oldTextureControlJson != null) { textureControl.RestoreFromJSON(new JSONClass()); textureControl.RestoreFromJSON(oldTextureControlJson); } // Set the gen and pelivs material options to match the rest of the skin var skinMaterialOpts = containingAtom.GetStorableByID("skin") as DAZCharacterMaterialOptions; genMaterialOptions.CopyMaterialParams(skinMaterialOpts); // We don't want the textures in character skin options to apply to the male pelvis which they do by default. genMaterialOptions.ClearCustomTextures(); if (savedPresetJson != null) { var genMatOptionsJson = savedPresetJson["genMaterialOpts"] as JSONClass; if (genMatOptionsJson != null && genMaterialOptions != null) { genMaterialOptions.RestoreFromJSON(new JSONClass()); // Set all to default genMaterialOptions.RestoreFromJSON(genMatOptionsJson); } } } catch (Exception e) { SuperController.LogError($"Error copying skin options from previous skin. {e}"); } } // The texture control that controls the body's textures from the "Skin Textures" tab // doesn't go through material options. It sets textures on the material directly. private void SyncFemalePelvisTextures(MaterialOptions pelvisOpts, DAZCharacterTextureControl textureControl) { var bodypart = "genitals"; pelvisOpts.GetUrlJSONParam("customTexture" + pelvisOpts.textureGroup1.textureName).val = textureControl.GetUrlJSONParam(bodypart + "DiffuseUrl").val; pelvisOpts.GetUrlJSONParam("customTexture" + pelvisOpts.textureGroup1.secondaryTextureName).val = textureControl.GetUrlJSONParam(bodypart + "SpecularUrl").val; pelvisOpts.GetUrlJSONParam("customTexture" + pelvisOpts.textureGroup1.thirdTextureName).val = textureControl.GetUrlJSONParam(bodypart + "GlossUrl").val; //Fourth is alpha pelvisOpts.GetUrlJSONParam("customTexture" + pelvisOpts.textureGroup1.fifthTextureName).val = textureControl.GetUrlJSONParam(bodypart + "NormalUrl").val; pelvisOpts.GetUrlJSONParam("customTexture" + pelvisOpts.textureGroup1.sixthTextureName).val = textureControl.GetUrlJSONParam(bodypart + "DecalUrl").val; } private string GetPathToObject(GameObject go) { if (go.transform.parent == null || go.transform.parent == containingAtom.gameObject.transform) return "/" + go.name; return GetPathToObject(go.transform.parent.gameObject) + "/" + go.name; } // Change the status of the male portions of the model. Specifically we only want the rigid bodies and controllers private void SetMaleTransformsActive(bool active) { foreach(GameObject go in maleObjects) { go.SetActive(active); } foreach(var ac in genAutoColliders) { ac.gameObject.SetActive(active); } } private void UpdateTestesCollision(bool disable) { // Get the testes rigidbody from the atom foreach (Rigidbody rb in containingAtom.rigidbodies) { if (rb.name == "Testes") { rb.detectCollisions = !disable; break; } } } private void UpdateAutoColliders() { foreach (var entry in maleAutoColliderConfigs) { var ac = entry.Key; ac.transformedAnchorTargetDirty = true; ac.colliderDirty = true; } foreach (var batchUpdater in characterRun.autoColliderUpdaters) { batchUpdater.UpdateAutoColliders(); } //characterRun.ResetMorphs(); characterRun.SetNeedsAutoColliderUpdate(); } private void SyncAutoColliderSettings() { foreach (GameObject go in maleObjects) { if (go.activeInHierarchy) { foreach (AutoCollider ac in genAutoColliderIgnoreList) { var ignoreList = ac.ignoreColliders.ToList(); if (!ignoreList.Contains(go.transform)) { ignoreList.Add(go.transform); ac.ignoreColliders = ignoreList.ToArray(); ac.InitColliders(); } } } } foreach (var maleAutoCollider in genAutoColliders) { if (maleAutoCollider.hardCollider != null) { // Gen colliders shouldn't collide with each other... foreach (var maleAutoCollider2 in genAutoColliders) { if (maleAutoCollider != maleAutoCollider2) { Physics.IgnoreCollision(maleAutoCollider.hardCollider, maleAutoCollider2.hardCollider); } } } IgnoreCollider(maleAutoCollider.hardCollider); IgnoreCollider(maleAutoCollider.jointCollider); } foreach (var maleCollider in genColliders) { IgnoreCollider(maleCollider); } foreach (var batchUpdater in characterRun.autoColliderUpdaters) { batchUpdater.UpdateAutoColliders(); } } private void IgnoreCollider(Collider collider) { if (collider == null) { return; } foreach (AutoCollider ac in genAutoColliderIgnoreList) { if (ac.jointCollider != null) { Physics.IgnoreCollision(collider, ac.jointCollider); } } foreach (Collider c in genColliderIgnoreList) { //SuperController.LogMessage($"{collider} {c}"); Physics.IgnoreCollision(collider, c); } var mesh = containingAtom.GetStorableByID("LowerPhysicsMesh") as DAZPhysicsMesh; foreach (DAZPhysicsMeshSoftVerticesGroup vertGroup in mesh.softVerticesGroups) { foreach (var softVertSet in vertGroup.softVerticesSets) { Physics.IgnoreCollision(softVertSet.jointCollider, collider); if (softVertSet.jointCollider2 != null) { Physics.IgnoreCollision(softVertSet.jointCollider2, collider); } } } } private void Update() { // For whatever reason, putting on clothes resets this; if (skin != null) { skin.materialsEnabled[29] = !hideMalePelvis.val && (showMaterial29 == null || showMaterial29.val); // Male Pelvis skin.materialsEnabled[31] = false; // Female Genital/Pelvis } // Deferred seam rebake: waiting for skinLoaded keeps pending requests // alive across scene load / skin reload until there is a skin to bake on. if (skinLoaded) { if (_seamRebakeAt >= 0f) { if (Time.unscaledTime >= _seamRebakeAt) { _seamRebakeAt = -1f; bool blendOn = seamColorBlendEnabled != null && seamColorBlendEnabled.val; if (blendOn && !MatchSkinColor() && _seamRebakeRetries < 5) { // Materials/textures may still be initializing; retry _seamRebakeRetries++; _seamRebakeAt = Time.unscaledTime + 1f; } else { GenerateSeamColorBlend(blendOn ? SeamColorBlendRadius : 0f); } } } else if (Time.unscaledTime >= _seamWatchAt) { _seamWatchAt = Time.unscaledTime + 1f; CheckSeamDecalIntegrity(); } } if (characterRun.morphBank3 != null) { // Fix these settings in case they got changed somehow if (characterRun.morphBank3 != characterSelector.maleMorphBank2) { characterRun.morphBank3 = characterSelector.maleMorphBank2; } if (characterRun.morphBank3.connectedMesh != null) { characterRun.morphBank3.connectedMesh = null; } // For whatever reason, the threaded morph logic in DAZCharacterRun // just doesn't bother with the 2nd graft. So we'll just do it manually here if (characterRun.morphBank3.bonesDirty) { characterRun.WaitForRunTask(); characterRun.bones.SetMorphedTransform(false); characterRun.morphBank3.bonesDirty = false; } if (characterRun.morphBank3.boneRotationsDirty != null && characterRun.morphBank3.boneRotationsDirty.Count > 0) { characterRun.WaitForRunTask(); foreach (var entry in characterRun.morphBank3.boneRotationsDirty) { entry.Key.SyncMorphBoneRotations(false); } characterRun.morphBank3.boneRotationsDirty.Clear(); } } } private void UnloadSkin() { status.val = "unloaded"; ResetSeamBlendState(); skin = null; if(genMaterialOptions != null) { newCharacter.containingAtom.UnregisterAdditionalStorable(genMaterialOptions); } genMaterialOptions = null; if(maleCharacterContainer != null) { Destroy(maleCharacterContainer); } if(newCharacterContainer != null) { Destroy(newCharacterContainer); } if(maleAsset != null) { Destroy(maleAsset); } maleCharacterContainer = null; newCharacterContainer = null; } private void OnEnable() { SetMaleTransformsActive(true); ReloadSkin(); foreach(var entry in morphValues) { entry.Key.morphValue = entry.Value; } } private void OnDisable() { status.val = "disabled"; HideMaterialOptionsIUI(); //RunRudolf, get back to main UI, because otherwise the UI will be empty when re-enabling the plugin SetMaleTransformsActive(false); if(characterSelector != null && previousSelected != null) { characterSelector.selectedCharacter = previousSelected; } foreach(var morph in morphs) { morphValues[morph] = morph.morphValue; morph.Reset(); } } private void OnDestroy() { if(marker != null) { marker.Unregister(); } UnloadSkin(); foreach(var morph in morphs) { morph.Reset(); } if(maleAsset != null) { Destroy(maleAsset); } // Undo the hidden-penis morph override only if it is actually applied; // restoring unconditionally would clobber the character's own morph // values (Penis Length etc.) with stale or default restore values. if (hiddenMorphsApplied != null && hiddenMorphsApplied.val) { if (genZMeshOffsetMorph != null) genZMeshOffsetValue.val = genZMeshOffsetValue_restore.val; //if (genYMeshOffsetValue != null) genYMeshOffsetValue.val = genYMeshOffsetValue_restore.val; if (penisLengthMorph != null) penisLengthMorph.morphValue = penisLengthMorph_restore.val; if (genitalBulge1Morph != null) genitalBulge1Morph.morphValue = genitalBulge1Morph_restore.val; if (scCastrateMorph != null) scCastrateMorph.morphValue = scCastrateMorph_restore.val; if (genitalAreaInOutMorph != null) genitalAreaInOutMorph.morphValue = genitalAreaInOutMorph_restore.val; if (monsPubicInOutMorph != null) monsPubicInOutMorph.morphValue = monsPubicInOutMorph_restore.val; // Reset testes collision UpdateTestesCollision(disableTestesCollision_restore.val); } Skynet.ToolTipManager.Destroy(); //RunRudolf } List GetAllChildren(GameObject parent, List objectList = null) { if (objectList == null) objectList = new List(); foreach (Transform child in parent.transform) { objectList.Add(child.gameObject); GetAllChildren(child.gameObject, objectList); } return objectList; } public override void RestoreFromJSON(JSONClass jc, bool restorePhysical = true, bool restoreAppearance = true, JSONArray presetAtoms = null, bool setMissingToDefault = true) { savedPresetJson = jc; if(jc["baseSkinMaterialOpts"] != null) { var optsList = new List(); foreach(var optsJson in jc["baseSkinMaterialOpts"].AsArray) { optsList.Add(optsJson as JSONClass); } oldSkinOpts = optsList.ToArray(); } if(jc["baseTextureControl"] != null) { oldTextureControlJson = jc["baseTextureControl"].AsObject; } base.RestoreFromJSON(jc, restorePhysical, restoreAppearance, presetAtoms, setMissingToDefault); } public override JSONClass GetJSON(bool includePhysical = true, bool includeAppearance = true, bool forceStore = false) { var jsonClass = base.GetJSON(true, true, true); if(genMaterialOptions != null) jsonClass.Add("genMaterialOpts", genMaterialOptions.GetJSON(true,true,true)); if(newCharacter != null) { JSONArray baseSkinMaterialOpts = new JSONArray(); foreach(var options in newCharacter.GetComponentsInChildren()) { baseSkinMaterialOpts.Add(options.GetJSON(true,true,true)); } jsonClass.Add("baseSkinMaterialOpts", baseSkinMaterialOpts); var textureControl = newCharacter.GetComponentInChildren(); if(textureControl != null) jsonClass.Add("baseTextureControl", textureControl.GetJSON(true,true,true)); } return jsonClass; } private void LoadPreset(string path) { if (string.IsNullOrEmpty(path)) return; var presetJson = (JSONClass)LoadJSON(path); // Adjust the material options first. savedPresetJson = presetJson; if(!mergeLoad.val){ if(savedPresetJson["baseSkinMaterialOpts"] != null){ var optsList = new List(); foreach(var optsJson in savedPresetJson["baseSkinMaterialOpts"].AsArray){ optsList.Add(optsJson as JSONClass); } oldSkinOpts = optsList.ToArray(); } if(savedPresetJson["baseTextureControl"] != null){ oldTextureControlJson = savedPresetJson["baseTextureControl"].AsObject; } } else { oldSkinOpts = null; oldTextureControlJson = null; } RestoreSavedMaterialOptions(); // Restore the other settings. This might trigger a reload RestoreFromJSON(presetJson, true, true, null, false); } private void SavePreset(string path) { SuperController.singleton.fileBrowserUI.fileFormat = null; if (string.IsNullOrEmpty(path)) { return; } if (!path.ToLower().EndsWith($".{altFutaFileExt}")) { path += $".{altFutaFileExt}"; } SaveJSON(GetJSON(true,true,true), path); } // Function taken from VAMDeluxe's code public static string GetPluginPath(MVRScript plugin, Atom atom) { SuperController.singleton.currentSaveDir = SuperController.singleton.currentLoadDir; string pluginId = plugin.storeId.Split('_')[0]; MVRPluginManager manager = atom.GetStorableByID("PluginManager") as MVRPluginManager; string pathToScriptFile = manager.GetJSON(true, true)["plugins"][pluginId].Value; string pathToScriptFolder = pathToScriptFile.Substring(0, pathToScriptFile.LastIndexOfAny(new char[] { '/', '\\' })); return pathToScriptFolder; } private string FindPluginID(Atom atom, string suffix) { for (int i = 0; i < 20; i++) { var storable = atom.GetStorableByID($"plugin#{i}_{suffix}"); if (storable != null) { //SuperController.LogMessage($"FindPlugin: {suffix} - Found: {storable.storeId}"); return storable.storeId; } } return null; } // MacGruber / Discord 20.10.2020 // Get path prefix of the package that contains this plugin private static string GetPackageId(MVRScript script) { string id = script.name.Substring(0, script.name.IndexOf('_')); string filename = script.manager.GetJSON()["plugins"][id].Value; int idx = filename.IndexOf(":/", StringComparison.Ordinal); return idx >= 0 ? filename.Substring(0, idx) : null; } // Code for decent UI menue titles by hazmhox's VAMM plugin, see https://hub.virtamate.com/resources/vammoan.3134/. private JSONStorableString createStaticDescriptionTextMain( string DescTitle, string DescText, bool rightSide, int fieldHeight ) { JSONStorableString staticDescString = new JSONStorableString(DescTitle,DescText); UIDynamicTextField staticDescStringField = CreateTextField(staticDescString, rightSide); staticDescStringField.backgroundColor = new Color(1f, 1f, 1f, 0f); staticDescStringField.textColor = new Color(1f, 1f, 1f, 1f); LayoutElement sdsfLayout = staticDescStringField.GetComponent(); sdsfLayout.preferredHeight = sdsfLayout.minHeight = fieldHeight; staticDescStringField.height = fieldHeight; DisableScrollOnText(staticDescStringField); return staticDescString; } private JSONStorableString createStaticDescriptionText( string DescTitle, string DescText, bool rightSide, int fieldHeight) { JSONStorableString staticDescString = new JSONStorableString(DescTitle,DescText); UIDynamicTextField staticDescStringField = CreateTextField(staticDescString, rightSide); staticDescStringField.backgroundColor = new Color(1f, 1f, 1f, 0f); staticDescStringField.textColor = new Color(1f, 1f, 1f, 1f); LayoutElement sdsfLayout = staticDescStringField.GetComponent(); sdsfLayout.preferredHeight = sdsfLayout.minHeight = fieldHeight; staticDescStringField.height = fieldHeight; DisableScrollOnText(staticDescStringField); descriptionTextFields.Add(staticDescStringField); return staticDescString; } public static void DisableScrollOnText(UIDynamicTextField target) { // THIS is important, it's a "useless" temporary canvas group that will prevent the event from the mousewheel CanvasGroup tmpCG = target.UItext.transform.parent.transform.parent.transform.parent.gameObject.AddComponent(); tmpCG.blocksRaycasts = false; ScrollRect targetSR = target.UItext.transform.parent.transform.parent.transform.parent.GetComponent(); if( targetSR != null ) { targetSR.horizontal = false; targetSR.vertical = false; } } public JSONStorableFloat AddSlider(string name, float defaultVal, float min, float max, out UIDynamicSlider uiSlider, JSONStorableFloat.SetFloatCallback onChange = null, bool rangeAdjust = false, bool rightSide = false) { JSONStorableFloat storableFloat = new JSONStorableFloat(name, defaultVal, min, max, true, true); storableFloat.storeType = JSONStorableParam.StoreType.Full; uiSlider = CreateSlider(storableFloat, rightSide); uiSlider.rangeAdjustEnabled = rangeAdjust; uiSlider.valueFormat = "F4"; RegisterFloat(storableFloat); if(onChange != null){ storableFloat.setCallbackFunction = onChange; } return storableFloat; } private void SetupButtonWithLayout(UIDynamicButton target, float newSize) { ContentSizeFitter tmpCSF = target.button.transform.gameObject.AddComponent(); tmpCSF.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.MinSize; LayoutElement tmpLE = target.button.transform.gameObject.GetComponent(); RectTransform tmpRectT = target.button.transform.GetComponent(); Rect tmpRect = tmpRectT.rect; tmpRectT.pivot = new Vector2(0f, 0.5f); tmpLE.minWidth = newSize; tmpLE.preferredWidth = newSize; } private void SetupButtonWithoutLayout(UIDynamicButton target, float newSize, Vector2 newPosition) { ContentSizeFitter tmpCSF = target.button.transform.gameObject.AddComponent(); tmpCSF.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.MinSize; LayoutElement tmpLE = target.button.transform.gameObject.GetComponent(); RectTransform tmpRectT = target.button.transform.GetComponent(); Rect tmpRect = tmpRectT.rect; tmpRectT.pivot = new Vector2(0f, 0.5f); tmpLE.minWidth = newSize; tmpLE.preferredWidth = newSize; tmpLE.ignoreLayout = true; tmpRectT.anchoredPosition = newPosition; ignoreLayoutUI.Add((UIDynamic) target); } private void SetupToggleWithLayout(UIDynamicToggle target, float newSize) { ContentSizeFitter tmpCSF = target.toggle.transform.gameObject.AddComponent(); tmpCSF.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.MinSize; LayoutElement tmpLE = target.toggle.transform.gameObject.GetComponent(); RectTransform tmpRectT = target.toggle.transform.GetComponent(); Rect tmpRect = tmpRectT.rect; tmpRectT.pivot = new Vector2(0f, 0.5f); tmpLE.minWidth = newSize; tmpLE.preferredWidth = newSize; } private void SetupToggleWithoutLayout(UIDynamicToggle target, float newSize, Vector2 newPosition) { ContentSizeFitter tmpCSF = target.toggle.transform.gameObject.AddComponent(); tmpCSF.horizontalFit = UnityEngine.UI.ContentSizeFitter.FitMode.MinSize; LayoutElement tmpLE = target.toggle.transform.gameObject.GetComponent(); RectTransform tmpRectT = target.toggle.transform.GetComponent(); Rect tmpRect = tmpRectT.rect; tmpRectT.pivot = new Vector2(0f, 0.5f); tmpLE.minWidth = newSize; tmpLE.preferredWidth = newSize; tmpLE.ignoreLayout = true; tmpRectT.anchoredPosition = newPosition; ignoreLayoutUI.Add((UIDynamic) target); } // TODO: Move these to a separate file maybe... List RemovePins = new List() { 1190,1188,1198,1189, 1200,1186,1192,1119, 1120,1113,1129,1116, 1117,1115,1127,1104, 1105,1128,1103,1130, 1102,1121,1193,1201, 1178,1199,1179,1180, 1177,1122,1107,1233, 6122,16844, // 1140 }; List HideHipVerts = new List() { // 18583, // 18600, // 18597, // 18596, // 18840,18836,18848,18837, // 18839,18838,8164,8165, // 8166,8163,8176,8162, // 8167,7918,7919,7922, // 7905, }; List PelvisPolysTopRow = new List() { 14070, 14072, 14074, 14075, 13311, 13313, 13315, 13316, 13344, 13345, 13346, 13347, 13348, 13357, 13359, 14103, 14104, 14105, 14106, 14107, 14116, 14118, }; Dictionary FemaleGenPins = new Dictionary { //test [159] = 2781, //[158] = 8194, [1348] = 7816, //[649] = 8186, [649] = 7859, [279] = 8196, //[674] = 8184, [674] = 2778, // test r [756] = 13589, [1343] = 18496, [1349] = 18496, //[1240] = 18857, [1240] = 18539, [885] = 18867, // Double-layer Left [33] = 8215, [4] = 2326, [2] = 2311, [7] = 6122, [6] = 6121, [0] = 334, [5] = 2777, // Double-layer right [67] = 18886, [39] = 13156, [37] = 13141, [42] = 16844, [41] = 16843, [36] = 11262, [40] = 13586, }; Dictionary FemaleGenPinsShowVagina = new Dictionary { // Double-layer Left [33] = 8215, [4] = 2326, [2] = 2311, [7] = 6122, [6] = 6121, [0] = 334, [5] = 2777, // Double-layer right [67] = 18886, [39] = 13156, [37] = 13141, [42] = 16844, [41] = 16843, [36] = 11262, [40] = 13586, // Extra [1296] = 7775, [116] = 7817, [1298] = 18455, [714] = 18497, //[1311] = 13142, //[1308] = 2312, // scrotum /*[1309] = 2313, [125] = 2314, [103] = 6766, [1310] = 13143, [723] = 13144, [701] = 17475,*/ [701] = 13144, [103] = 2314, [732] = 17475, [134] = 6766, [125] = 2313, [1309] = 2312, [1310] = 13142, [723] = 13143, // clit }; Dictionary PerineumPins = new Dictionary { // Top Row /* [1202] = 18583, [1212] = 18600, [1210] = 18597, [1209] = 18596, [1222] = 18840, [1218] = 18836, [1230] = 18848, [1219] = 18837, [1221] = 18839, [1220] = 18838, [1150] = 8164, [1151] = 8165, [1152] = 8166, [1149] = 8163, [1162] = 8176, [1148] = 8162, [1153] = 8167, [1138] = 7918, [1139] = 7919, [1141] = 7922, [1131] = 7905,*/ //Second Row /* [1214] = 18621, [1206] = 18591, [1185] = 13750, [1227] = 18845, [1228] = 18846, [1229] = 18847, [1231] = 18849, [1216] = 18834, [1217] = 18835, [1215] = 18833, [1144] = 8158, [1145] = 8159, [1147] = 8161, [1146] = 8160, [1163] = 8177, [1161] = 8175, [1160] = 8174, [1159] = 8173, [1112] = 2956, [1135] = 7913, [1143] = 7943,*/ // Left [1118] = 6122, [1117] = 6121, [1104] = 334, // [1105] = 2777, [1128] = 2781, [1103] = 7816, // [1130] = 8186, //4801 + 3058; // [1130] = 7210 + 3233, [1130] = 7859, [1102] = 8196, // [1107] = 8184, [1107] = 2778, // [1191] = 16844, [1190] = 16843, [1179] = 11262, [1180] = 13586, [1199] = 13589, [1178] = 18496, //[1201] = 18857, [1201] = 18539, [1177] = 18867, }; Dictionary PerineumPinsShowVagina = new Dictionary { // Top Row /* [1202] = 18583, [1212] = 18600, [1210] = 18597, [1209] = 18596, [1222] = 18840, [1218] = 18836, [1230] = 18848, [1219] = 18837, [1221] = 18839, [1220] = 18838, [1150] = 8164, [1151] = 8165, [1152] = 8166, [1149] = 8163, [1162] = 8176, [1148] = 8162, [1153] = 8167, [1138] = 7918, [1139] = 7919, [1141] = 7922, [1131] = 7905,*/ //Second Row /* [1214] = 18621, [1206] = 18591, [1185] = 13750, [1227] = 18845, [1228] = 18846, [1229] = 18847, [1231] = 18849, [1216] = 18834, [1217] = 18835, [1215] = 18833, [1144] = 8158, [1145] = 8159, [1147] = 8161, [1146] = 8160, [1163] = 8177, [1161] = 8175, [1160] = 8174, [1159] = 8173, [1112] = 2956, [1135] = 7913, [1143] = 7943,*/ // inner thighs [1194] = 18455, [1256] = 18497, [1123] = 7775, [1255] = 7817, // [307] = 2312, // [743] = 13142, // scrotum /*[309] = 2313, [312] = 2314, [310] = 6766, [311] = 6765, [745] = 13143, [747] = 13144, [746] = 17475,*/ [311] = 6765, [310] = 6766, [746] = 17475, [312] = 2314, [747] = 13144, [309] = 2313, [745] = 13143, [307] = 2312, [743] = 13142, }; List VisibleFemaleGenVerts = new List() { 1431, 1423, 1429, 1430, 1426, 1415, 1414, 1413, 1418, 1417, 1412, 1416, 1425, 1424, 1451, 1443, 1449, 1450, 1446, 1435, 1434, 1433, 1438, 1437, 1432, 1436, 1445, 1444, // back line 8187, 8252, 7189, 7840, 7868, 7820, 7868, 7854, 7874, 18554, 18534, 18548, 18500, 18520, 17896, 18858, 18923, // second line 8197, 8248, 11, 1261, 1301, 155, 156, 1300, 1297, 1299, 753, 752, 1302, 1263, 46, 18919, 18868, 12, 9, 1323, 1303, 1341, 1339, 1337, 1338, 1340, 1304, 1324, 44, 47, 43, 18866, 8195, 8, 2777, 8195, 157, 1319, 644, 646, 1330, 647, 290, 143, 893, 1238, 1334, 1237, 1235, 1321, 754, 8194, 1320, 648, 645, 151, 152, 1329, 153, 154, 75, 751, 750, 1331, 749, 748, 1236, 1239, 1322, 755, 741, 145, 1350, 146, 147, 1325, 148, 149, 144, 746, 745, 1332, 744, 743, 1358, 742, 1345, 1356, 1355, 1354, 150, 676, 747, 1357, 1360, 1361, 1344, 1234, 1233, 721, 675, 123, 642, 643, // ? 158, 673, 528, 1328, 527, 526, 1353, 525, 1353, 525, 1113, 1114, 1363, 1115, 1116, 1335, 1116, 1335, 1116, 1335, 1116, 1336, 1117, 1118, 1119, 529, 528, 672, 295, 294, 1327, 292, 293, 292, 1352, 291, 79, 894, 1362, 895, 896, 901, 896, 901, 1335, 897, 898, // 278, }; List HideFemalePelvisVertsWhenShowingVagina = new List() { 1439, 56, 55, 54, 63, 59, 64, 60, 62, 61, 26, 27, 28, 25, 30, 24, 29, 19, 20, 21, 1419, 1442, 53, 1262, 1276, 1275, 678, 65, 58, 677, 57, 70, 22, 80, 23, 31, 1259, 1272, 1271, 1260, 18, 1422, 1284, 1294, 1295, 1282, 1290, 1291, 1283, 1286, 1287, 1285, 1265, 1266, 1264, 1279, 1288, 1289, 1280, 1292, 1293, 1281, 681, 1277, 1278, 680, 1269, 1270, 679, 71, 81, 1267, 1268, 82, 1273, 83, 1274, 684, 683, 682, 72, 84, 85, 86, 687, 686, 685, 665, 87, 88, 89, // fix 160, 757, }; List HidePerineumVerts = new List() { 1104, 1105, 1128, 1130, 1103, 1102, 1107, 1177, 1201, 1178, 1199, 1180, 1179, 1180, 1199, 1193, 1122, 1121, 1117, 1115, 1127, 1116, 1129, //1113, //1120, //1119, //1192, //1186, 1200, 1189, 1198, 1188, 1190, 1118, 1114, 1126, 1264, //1262, //1260, //1258, //1257, //1259, //1261, //1263, 1265, 1197, 1187, 1191, }; List removeIndividualVagVerts = new List() { // 122, 278, 1342, 3233 }; List visibleVagPolys = new List() { 0, 6, 7, 8, 9, 10, 11, 12, 13, 23, 24, 35, 36, 37, 38, 39, 40, 41, 42, 52, 53, 140, 141, 142, 143, 337, 338, 339, 340, 341, 342, 343, 344, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 463, 465, 466, 467, 468, 617, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 777, 778, 779, 780, 781, 782, 783, 784, 1028, 1029, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1151, 1266, 1267, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1324, 1325, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1392, 1393, 1394, 1395, 1396, 1397, 1400, 1401, 1402, 1403, 1404, 1405, 1407, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, 1416, 1419, 1420, 1421, 1422, 1423, 1424, 1426, 1427, 1428, 1429, }; private Color GetAverageColor(Texture mainTex) { if (mainTex == null) return Color.white; RenderTexture tmp = RenderTexture.GetTemporary(16, 16, 0, RenderTextureFormat.Default, RenderTextureReadWrite.sRGB); Graphics.Blit(mainTex, tmp); RenderTexture previous = RenderTexture.active; RenderTexture.active = tmp; Texture2D myTex = new Texture2D(16, 16, TextureFormat.RGB24, false); myTex.ReadPixels(new Rect(0, 0, 16, 16), 0, 0); myTex.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(tmp); Color[] pixels = myTex.GetPixels(); float r = 0, g = 0, b = 0; foreach (var p in pixels) { r += p.r; g += p.g; b += p.b; } UnityEngine.Object.Destroy(myTex); float count = pixels.Length; return new Color(r / count, g / count, b / count); } // VaM skins render through GPU compute skinning, not a SkinnedMeshRenderer, // so get the UV-mapped mesh from the DAZ mesh data instead. private Mesh GetSkinMesh() { if (skin == null) return null; SkinnedMeshRenderer smr = skin.GetComponent(); if (smr != null && smr.sharedMesh != null) return smr.sharedMesh; if (skin.dazMesh != null) { if (skin.dazMesh.morphedUVMappedMesh != null) return skin.dazMesh.morphedUVMappedMesh; if (skin.dazMesh.uvMappedMesh != null) return skin.dazMesh.uvMappedMesh; } return null; } // Finds vertices on open (unshared) edges of the given triangle list. private HashSet FindOpenBoundaryVerts(List tris) { Dictionary edgeCounts = new Dictionary(); for (int i = 0; i < tris.Count; i += 3) { for (int e = 0; e < 3; e++) { int v1 = tris[i + e]; int v2 = tris[i + (e + 1) % 3]; long minV = Mathf.Min(v1, v2); long maxV = Mathf.Max(v1, v2); long edgeKey = (minV << 32) | maxV; if (edgeCounts.ContainsKey(edgeKey)) edgeCounts[edgeKey]++; else edgeCounts[edgeKey] = 1; } } HashSet boundaryVerts = new HashSet(); foreach (var kvp in edgeCounts) { if (kvp.Value == 1) { boundaryVerts.Add((int)(kvp.Key >> 32)); boundaryVerts.Add((int)(kvp.Key & 0xFFFFFFFF)); } } return boundaryVerts; } // Returns graft vertices lying on the connection seam. Prefers open boundary // edges; if the graft is welded to the body (no open edges), falls back to // matching graft vertex positions against body vertex positions. private HashSet FindSeamVerts(Mesh m, List graftTris, Vector3[] verts) { HashSet boundary = FindOpenBoundaryVerts(graftTris); if (boundary.Count > 0) return boundary; HashSet graftVerts = new HashSet(graftTris); Dictionary bodyByPos = new Dictionary(); for (int s = 0; s < m.subMeshCount; s++) { if (s == 29 || s == 30) continue; int[] tris = m.GetTriangles(s); foreach (int v in tris) { if (graftVerts.Contains(v)) continue; bodyByPos[QuantizePos(verts[v])] = v; } } HashSet seam = new HashSet(); foreach (int v in graftVerts) { if (bodyByPos.ContainsKey(QuantizePos(verts[v]))) seam.Add(v); } return seam; } private Vector3 QuantizePos(Vector3 p) { // 0.5mm grid, coarse enough to catch welded/glued duplicate vertices return new Vector3(Mathf.Round(p.x * 2000f), Mathf.Round(p.y * 2000f), Mathf.Round(p.z * 2000f)); } // Copies a (possibly non-readable) texture into a CPU-readable Texture2D. // Color textures (diffuse/decal) use sRGB; data textures (spec/gloss/normal // maps) must use linear=true so values round-trip unchanged. private Texture2D ReadTextureToCPU(Texture tex, int size, bool linear = false) { if (tex == null) return null; RenderTexture tmp = RenderTexture.GetTemporary(size, size, 0, RenderTextureFormat.Default, linear ? RenderTextureReadWrite.Linear : RenderTextureReadWrite.sRGB); Graphics.Blit(tex, tmp); RenderTexture previous = RenderTexture.active; RenderTexture.active = tmp; Texture2D result = new Texture2D(size, size, TextureFormat.ARGB32, false, linear); result.ReadPixels(new Rect(0, 0, size, size), 0, 0); result.Apply(); RenderTexture.active = previous; RenderTexture.ReleaseTemporary(tmp); return result; } // Samples texture colors right at the graft seam: the open boundary of the // penis/pelvis submeshes on the graft side, and body vertices within a small // radius of that boundary on the body side. When seamVertColors is given it // also receives a local body color per seam vertex, so the blend can follow // the body's spatial color variation (bright mons vs dark perineum) instead // of one flat average that mismatches everywhere. private bool TryGetSeamColors(out Color graftTexAvg, out Color bodyFinalAvg, out Color patchAvg, float patchRadius) { graftTexAvg = Color.white; bodyFinalAvg = Color.white; patchAvg = Color.white; if (skin == null || skin.GPUmaterials == null) return false; Mesh m = GetSkinMesh(); if (m == null) return false; Vector3[] verts = m.vertices; Vector2[] uvs = m.uv; if (verts == null || verts.Length == 0 || uvs == null || uvs.Length < verts.Length) return false; // Same graft submesh selection as the seam gradient bake (29 = Pelvis, 30 = Penis) int[] graftSubmeshes; if (hideMalePelvis != null && hideMalePelvis.val) graftSubmeshes = new int[] { 30 }; else graftSubmeshes = new int[] { 29, 30 }; List graftTris = new List(); Dictionary vertSubmesh = new Dictionary(); foreach (int subIdx in graftSubmeshes) { if (subIdx >= m.subMeshCount) continue; int[] tris = m.GetTriangles(subIdx); graftTris.AddRange(tris); foreach (int v in tris) vertSubmesh[v] = subIdx; } if (graftTris.Count == 0) return false; HashSet boundaryVerts = FindSeamVerts(m, graftTris, verts); if (boundaryVerts.Count == 0) return false; Dictionary readableCache = new Dictionary(); try { // Graft side: sample the graft textures at the boundary vertices. // Texture color only (no diffuse) because we solve for a new diffuse. float gr = 0f, gg = 0f, gb = 0f; int gCount = 0; foreach (int v in boundaryVerts) { int subIdx; if (!vertSubmesh.TryGetValue(v, out subIdx)) continue; if (subIdx >= skin.GPUmaterials.Length || skin.GPUmaterials[subIdx] == null) continue; Texture tex = skin.GPUmaterials[subIdx].GetTexture("_MainTex"); if (tex == null) continue; Texture2D readable; if (!readableCache.TryGetValue(tex, out readable)) { readable = ReadTextureToCPU(tex, 256); readableCache[tex] = readable; } if (readable == null) continue; Vector2 uv = uvs[v]; Color c = readable.GetPixelBilinear(uv.x - Mathf.Floor(uv.x), uv.y - Mathf.Floor(uv.y)); gr += c.r; gg += c.g; gb += c.b; gCount++; } if (gCount == 0) return false; graftTexAvg = new Color(gr / gCount, gg / gCount, gb / gCount); // Body side: vertices of every non-graft submesh close to the seam ring. // Sampled color includes that material's diffuse tint (final rendered color). HashSet graftVertSet = new HashSet(graftTris); float br = 0f, bg = 0f, bb = 0f; int bCount = 0; float[] searchRadii = new float[] { 0.02f, 0.05f }; foreach (float maxDist in searchRadii) { br = 0f; bg = 0f; bb = 0f; bCount = 0; // Expanded bounding box of the seam ring for cheap rejection. Vector3 bbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 bbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); foreach (int bv in boundaryVerts) { Vector3 p = verts[bv]; bbMin = Vector3.Min(bbMin, p); bbMax = Vector3.Max(bbMax, p); } bbMin -= new Vector3(maxDist, maxDist, maxDist); bbMax += new Vector3(maxDist, maxDist, maxDist); for (int subIdx = 0; subIdx < m.subMeshCount; subIdx++) { if (subIdx == 29 || subIdx == 30) continue; if (subIdx >= skin.GPUmaterials.Length) continue; // Invisible submeshes (e.g. 31, the hidden female pelvis top) // must not contribute: their textures can differ wildly from // the skin actually visible around the seam. if (skin.materialsEnabled != null && subIdx < skin.materialsEnabled.Length && !skin.materialsEnabled[subIdx]) continue; Material mat = skin.GPUmaterials[subIdx]; if (mat == null) continue; Texture tex = mat.GetTexture("_MainTex"); if (tex == null) continue; int[] tris = m.GetTriangles(subIdx); if (tris.Length == 0) continue; Texture2D readable = null; Color diffuse = mat.HasProperty("_Color") ? mat.GetColor("_Color") : Color.white; HashSet bodyVerts = new HashSet(tris); foreach (int v in bodyVerts) { if (graftVertSet.Contains(v)) continue; Vector3 pos = verts[v]; if (pos.x < bbMin.x || pos.y < bbMin.y || pos.z < bbMin.z || pos.x > bbMax.x || pos.y > bbMax.y || pos.z > bbMax.z) continue; bool near = false; foreach (int bv in boundaryVerts) { if (Vector3.Distance(pos, verts[bv]) <= maxDist) { near = true; break; } } if (!near) continue; if (readable == null) { if (!readableCache.TryGetValue(tex, out readable)) { // Body textures need decent resolution here: the seam // colors must match the actual texels right at the seam, // and a heavily downscaled read blurs them into a color // that no longer exists on the rendered skin. readable = ReadTextureToCPU(tex, 1024); readableCache[tex] = readable; } if (readable == null) break; } Vector2 uv = uvs[v]; Color c = readable.GetPixelBilinear(uv.x - Mathf.Floor(uv.x), uv.y - Mathf.Floor(uv.y)); float fr = c.r * diffuse.r, fg = c.g * diffuse.g, fb = c.b * diffuse.b; br += fr; bg += fg; bb += fb; bCount++; } } if (bCount > 0) break; } if (bCount == 0) return false; bodyFinalAvg = new Color(br / bCount, bg / bCount, bb / bCount); // Coverage-weighted average: the wide patch replaces the look of // the whole area it covers, so its color must match that area's // average (weighted by patch opacity), not just the ring's own // neighborhood. The ring sits in slightly brighter skin, so a // ring-only average paints the patch a few percent too bright — // this is the measured-reality auto correction. patchAvg = bodyFinalAvg; if (patchRadius > 0f) { Vector3 pbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 pbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); foreach (int bv in boundaryVerts) { Vector3 p = verts[bv]; pbMin = Vector3.Min(pbMin, p); pbMax = Vector3.Max(pbMax, p); } pbMin -= new Vector3(patchRadius, patchRadius, patchRadius); pbMax += new Vector3(patchRadius, patchRadius, patchRadius); float pr = 0f, pg = 0f, pb = 0f, pw = 0f; for (int subIdx = 0; subIdx < m.subMeshCount; subIdx++) { if (subIdx == 29 || subIdx == 30) continue; if (subIdx >= skin.GPUmaterials.Length) continue; if (skin.materialsEnabled != null && subIdx < skin.materialsEnabled.Length && !skin.materialsEnabled[subIdx]) continue; Material mat = skin.GPUmaterials[subIdx]; if (mat == null) continue; Texture tex = mat.GetTexture("_MainTex"); if (tex == null) continue; int[] tris = m.GetTriangles(subIdx); if (tris.Length == 0) continue; Texture2D readable = null; Color diffuse = mat.HasProperty("_Color") ? mat.GetColor("_Color") : Color.white; HashSet bodyVerts = new HashSet(tris); foreach (int v in bodyVerts) { if (graftVertSet.Contains(v)) continue; Vector3 pos = verts[v]; if (pos.x < pbMin.x || pos.y < pbMin.y || pos.z < pbMin.z || pos.x > pbMax.x || pos.y > pbMax.y || pos.z > pbMax.z) continue; float minDist = float.MaxValue; foreach (int bv in boundaryVerts) { float dd = Vector3.Distance(pos, verts[bv]); if (dd < minDist) minDist = dd; } if (minDist > patchRadius) continue; float wgt = 1f - Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(minDist / patchRadius)); if (wgt <= 0f) continue; if (readable == null) { if (!readableCache.TryGetValue(tex, out readable)) { readable = ReadTextureToCPU(tex, 1024); readableCache[tex] = readable; } if (readable == null) break; } Vector2 uv = uvs[v]; Color c = readable.GetPixelBilinear(uv.x - Mathf.Floor(uv.x), uv.y - Mathf.Floor(uv.y)); // Average in LINEAR space: rendered brightness is linear // in the albedo, and averaging the gamma-encoded values // makes a flat fill render brighter than the textured // skin it replaces (convexity bias, a few percent). pr += Mathf.GammaToLinearSpace(c.r) * Mathf.GammaToLinearSpace(diffuse.r) * wgt; pg += Mathf.GammaToLinearSpace(c.g) * Mathf.GammaToLinearSpace(diffuse.g) * wgt; pb += Mathf.GammaToLinearSpace(c.b) * Mathf.GammaToLinearSpace(diffuse.b) * wgt; pw += wgt; } } if (pw > 0f) patchAvg = new Color( Mathf.LinearToGammaSpace(pr / pw), Mathf.LinearToGammaSpace(pg / pw), Mathf.LinearToGammaSpace(pb / pw)); } return true; } finally { foreach (var kvp in readableCache) { if (kvp.Value != null) UnityEngine.Object.Destroy(kvp.Value); } } } // Bakes a decal that gradually tints the graft toward the body's color at the // connection seam. Textures can never match perfectly, so instead of trying to // match them exactly this hides the transition under a smooth gradient. // Works with both the opaque and transparent shaders. private void GenerateSeamColorBlend(float fadeRadius) { if (skin == null || skin.GPUmaterials == null) return; Material penisMat = skin.GPUmaterials.Length > 30 ? skin.GPUmaterials[30] : null; Material pelvisMat = skin.GPUmaterials.Length > 29 ? skin.GPUmaterials[29] : null; if (penisMat == null) return; if (fadeRadius <= 0f) { if (_seamDecalApplied) { if (pelvisMat != null && pelvisMat.HasProperty("_DecalTex")) pelvisMat.SetTexture("_DecalTex", _seamDecalPrevPelvis); if (penisMat.HasProperty("_DecalTex")) penisMat.SetTexture("_DecalTex", _seamDecalPrevPenis); _seamDecalApplied = false; } RestoreBodySeamDecals(); RestoreGraftSurfaceMaps(); RestoreSkinColor(); return; } if (!penisMat.HasProperty("_DecalTex")) { SuperController.LogMessage("[AF debug] blend aborted: penis material has no _DecalTex."); return; } // Two very different seams need two very different treatments. With the // pelvis visible the seam is the long pelvis outline running through // evenly colored skin: a wide soft patch of the flat average color over // BOTH sides hides it completely (the original, known-good recipe). // With 'Hide Male Pelvis' the graft's cut ring is buried inside the // body, so the visible transition is where the shaft exits the body // surface instead: the fade is anchored to that surface (signed // distance) and only the graft is painted, never the body. bool hidePelvisMode = hideMalePelvis != null && hideMalePelvis.val; // OFF paints a patch of this size on both sides; resolve it up front // so the sampled color can be weighted by the actual coverage. if (!hidePelvisMode && seamPatchRadius != null) fadeRadius = seamPatchRadius.val; Color graftTexAvg; Color bodySeamColor; Color patchColorAvg; if (!TryGetSeamColors(out graftTexAvg, out bodySeamColor, out patchColorAvg, hidePelvisMode ? 0f : fadeRadius)) { // Right after a skin reload the textures may still be loading // asynchronously; retry a few times before giving up. if (_seamRebakeRetries < 5) { _seamRebakeRetries++; _seamRebakeAt = Time.unscaledTime + 1f; } else { SuperController.LogMessage("[AF debug] blend aborted: seam color sampling failed after retries."); } return; } _seamRebakeRetries = 0; // OFF paints with the coverage-weighted average so the patch matches // the area it replaces (the ring-only average runs a few percent // bright); keep the ring average around for the debug log. Color ringColor = bodySeamColor; if (!hidePelvisMode) bodySeamColor = patchColorAvg; Mesh m = GetSkinMesh(); Vector3[] mVerts = m != null ? m.vertices : null; Vector2[] mUvs = m != null ? m.uv : null; if (m == null || mVerts == null || mVerts.Length == 0 || mUvs == null || mUvs.Length < mVerts.Length) { SuperController.LogMessage("[AF debug] blend aborted: no usable mesh/UV data."); return; } int[] graftSubmeshes; if (hidePelvisMode) graftSubmeshes = new int[] { 30 }; else graftSubmeshes = new int[] { 29, 30 }; List allTris = new List(); foreach (int subIdx in graftSubmeshes) { if (subIdx < m.subMeshCount) allTris.AddRange(m.GetTriangles(subIdx)); } if (allTris.Count == 0) { SuperController.LogMessage("[AF debug] blend aborted: no graft triangles."); return; } Dictionary vertW = new Dictionary(); Dictionary vertWSurf = null; Dictionary vertCol = null; HashSet activeVerts = new HashSet(allTris); int dbgBuried = 0, dbgFaded = 0, dbgUnpainted = 0, dbgSamples = 0; bool dbgNormals = false; if (hidePelvisMode) { // Surface-anchored fade: weight 0 (opaque) while the vertex is // buried in the body, ramping to 1 as it rises up the shaft. vertWSurf = new Dictionary(); vertCol = new Dictionary(); if (!ComputeHiddenModeFade(activeVerts, mVerts, m, bodySeamColor, vertW, vertWSurf, vertCol, out dbgBuried, out dbgFaded, out dbgUnpainted, out dbgSamples, out dbgNormals)) { SuperController.LogMessage("[AF debug] blend aborted: no visible body geometry near the graft."); return; } } else { // Ring-anchored fade: distance from the pelvis outline, which here // is an actual visible seam on the surface. HashSet boundaryVerts = FindSeamVerts(m, allTris, mVerts); if (boundaryVerts.Count == 0) { SuperController.LogMessage("[AF debug] blend aborted: no seam boundary found."); return; } foreach (int v in activeVerts) { if (boundaryVerts.Contains(v)) { vertW[v] = 0f; continue; } float minDist = float.MaxValue; Vector3 pos = mVerts[v]; foreach (int bv in boundaryVerts) { float dist = Vector3.Distance(pos, mVerts[bv]); if (dist < minDist) minDist = dist; } vertW[v] = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(minDist / fadeRadius)); } } Texture2D gradient = BakeGradientForTris(allTris, vertW, mUvs, 1024); Texture2D colorsTex = hidePelvisMode ? BakeColorsForTris(allTris, vertCol, mUvs, 1024, bodySeamColor) : null; if (gradient == null || (hidePelvisMode && colorsTex == null)) { if (gradient != null) UnityEngine.Object.Destroy(gradient); if (colorsTex != null) UnityEngine.Object.Destroy(colorsTex); SuperController.LogMessage("[AF debug] blend aborted: gradient bake failed."); return; } // Second, much narrower gradient drives the surface-map detail fade // around the contact area (hide-pelvis mode only) float[] surfW = null; if (hidePelvisMode) { Texture2D surfGrad = BakeGradientForTris(allTris, vertWSurf, mUvs, 1024); if (surfGrad != null) { Color[] spx = surfGrad.GetPixels(); DilateGradient(spx, 1024, 3); surfW = new float[spx.Length]; for (int i = 0; i < spx.Length; i++) surfW[i] = spx[i].r; UnityEngine.Object.Destroy(surfGrad); } } // The shader multiplies the decal by the material's diffuse _Color, so // store colors pre-diffuse; dividing also keeps sampled colors above 1 // representable in the texture. Color graftDiffuse = penisMat.HasProperty("_Color") ? penisMat.GetColor("_Color") : Color.white; // Turn the gradient into a decal, fully opaque at the seam and fading // out with distance. Pelvis visible: flat average color held near-opaque // across the band (pow 5, the original recipe). Pelvis hidden: local // per-vertex colors with a true gradient (pow 2). float fadePower = hidePelvisMode ? 2f : 5f; // In hide-pelvis mode the paint keeps the penis texture's own shading // detail: tone comes from the body, detail from the graft, so the // painted base doesn't turn into a flat featureless patch. Color[] penisPx = null; float brightness = seamColorBrightness != null ? seamColorBrightness.val : 1f; if (hidePelvisMode) { Texture pt = penisMat.GetTexture("_MainTex"); Texture2D ptRead = pt != null ? ReadTextureToCPU(pt, 1024) : null; if (ptRead != null) { penisPx = ptRead.GetPixels(); UnityEngine.Object.Destroy(ptRead); } } Color[] px = gradient.GetPixels(); DilateGradient(px, 1024, 3); Color[] colPx = colorsTex != null ? colorsTex.GetPixels() : null; for (int i = 0; i < px.Length; i++) { float w = AdjustSeamWeight(px[i].r, fadePower); Color c = colPx != null ? colPx[i] : bodySeamColor; if (penisPx != null) { Color pt = penisPx[i]; c.r *= graftTexAvg.r > 0.01f ? pt.r / graftTexAvg.r : 1f; c.g *= graftTexAvg.g > 0.01f ? pt.g / graftTexAvg.g : 1f; c.b *= graftTexAvg.b > 0.01f ? pt.b / graftTexAvg.b : 1f; } c.r *= brightness; c.g *= brightness; c.b *= brightness; px[i] = new Color( graftDiffuse.r > 0.01f ? c.r / graftDiffuse.r : c.r, graftDiffuse.g > 0.01f ? c.g / graftDiffuse.g : c.g, graftDiffuse.b > 0.01f ? c.b / graftDiffuse.b : c.b, 1f - w); } UnityEngine.Object.Destroy(gradient); if (colorsTex != null) UnityEngine.Object.Destroy(colorsTex); if (_seamColorDecalTex == null) { _seamColorDecalTex = new Texture2D(1024, 1024, TextureFormat.ARGB32, false); _seamColorDecalTex.wrapMode = TextureWrapMode.Repeat; } _seamColorDecalTex.SetPixels(px); _seamColorDecalTex.Apply(); if (!_seamDecalApplied) { _seamDecalPrevPelvis = (pelvisMat != null && pelvisMat.HasProperty("_DecalTex")) ? pelvisMat.GetTexture("_DecalTex") : null; _seamDecalPrevPenis = penisMat.GetTexture("_DecalTex"); _seamDecalApplied = true; } if (pelvisMat != null && pelvisMat.HasProperty("_DecalTex")) { pelvisMat.SetTexture("_DecalTex", _seamColorDecalTex); if (pelvisMat.HasProperty("_DecalAlpha")) pelvisMat.SetFloat("_DecalAlpha", 1f); } penisMat.SetTexture("_DecalTex", _seamColorDecalTex); if (penisMat.HasProperty("_DecalAlpha")) penisMat.SetFloat("_DecalAlpha", 1f); if (hidePelvisMode) { // The crotch must keep its own shading: only the graft adapts, the // body is never painted in this mode. RestoreBodySeamDecals(); // Fade the spec/gloss/normal detail in a narrow band around the // contact area so the material switch stops reading as a seam if (surfW != null) ApplyGraftSurfaceMapFade(penisMat, surfW); SuperController.LogMessage($"[AF debug] blend applied: mode=ON, fade={(seamFadeRange != null ? seamFadeRange.val : HiddenSeamFadeRadius):F3}, offset={(seamAnchorOffset != null ? seamAnchorOffset.val : 0f):F3}, brightness={brightness:F2}, surf range={(seamSurfaceRange != null ? seamSurfaceRange.val : 0.015f):F3}, buried={dbgBuried}, faded={dbgFaded}, unpainted={dbgUnpainted}, body samples={dbgSamples}, normals={dbgNormals}, detail={(penisPx != null)}, fallback color=({bodySeamColor.r:F2},{bodySeamColor.g:F2},{bodySeamColor.b:F2})"); } else { // Fade the body side toward the same flat color so both sides of // the seam meet inside one wide soft patch (the original recipe). // The body gets the same brightness-adjusted color as the graft, // otherwise the two halves of the patch no longer match. Color bodyPatchColor = new Color(bodySeamColor.r * brightness, bodySeamColor.g * brightness, bodySeamColor.b * brightness); ApplyBodySeamDecals(fadeRadius, bodyPatchColor); RestoreGraftSurfaceMaps(); SuperController.LogMessage($"[AF debug] blend applied: mode=OFF, radius={fadeRadius:F3}, brightness={brightness:F2}, patch color=({bodySeamColor.r:F3},{bodySeamColor.g:F3},{bodySeamColor.b:F3}), ring color=({ringColor.r:F3},{ringColor.g:F3},{ringColor.b:F3}), graft diffuse=({graftDiffuse.r:F2},{graftDiffuse.g:F2},{graftDiffuse.b:F2}), bodyDecalMats={_bodySeamDecalPrev.Count}"); } } // Hide-pelvis fade weights and colors. The graft's cut ring is buried // inside the body, so distances from it never line up with the visible // seam (log evidence: 83% of ring verts had no visible body vert within // 5cm). Instead the fade is anchored to the body surface itself: the // signed distance of a graft vertex from the nearest visible body vertex // (projected on that vertex's normal) is zero right where the shaft exits // the body — exactly where the transition is actually seen. Buried verts // are painted fully; each vertex takes the local body color around its // exit point. private bool ComputeHiddenModeFade(HashSet graftVerts, Vector3[] verts, Mesh m, Color fallbackColor, Dictionary vertW, Dictionary vertWSurf, Dictionary vertCol, out int buried, out int faded, out int unpainted, out int sampleCount, out bool usedNormals) { buried = 0; faded = 0; unpainted = 0; sampleCount = 0; Vector2[] uvs = m.uv; Vector3[] normals = m.normals; bool hasNormals = normals != null && normals.Length >= verts.Length; usedNormals = hasNormals; if (uvs == null || uvs.Length < verts.Length) return false; float fadeRange = seamFadeRange != null ? seamFadeRange.val : HiddenSeamFadeRadius; float anchorOffset = seamAnchorOffset != null ? seamAnchorOffset.val : 0f; // The surface-map detail fade is deliberately much narrower than the // color fade: a wide surface blend reads as worse than the seam itself. float surfRange = seamSurfaceRange != null ? seamSurfaceRange.val : 0.015f; // Bounds of the graft, expanded by the search range, limit the body scan Vector3 bbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 bbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); foreach (int v in graftVerts) { Vector3 p = verts[v]; bbMin = Vector3.Min(bbMin, p); bbMax = Vector3.Max(bbMax, p); } Vector3 margin = new Vector3(HiddenSeamSearchRange, HiddenSeamSearchRange, HiddenSeamSearchRange); bbMin -= margin; bbMax += margin; // Collect visible body vertices inside the bounds: position, normal // and final color (texel * material diffuse) List smpPos = new List(); List smpNrm = new List(); List smpCol = new List(); Dictionary readableCache = new Dictionary(); try { for (int subIdx = 0; subIdx < m.subMeshCount; subIdx++) { if (subIdx == 29 || subIdx == 30) continue; if (subIdx >= skin.GPUmaterials.Length) continue; if (skin.materialsEnabled != null && subIdx < skin.materialsEnabled.Length && !skin.materialsEnabled[subIdx]) continue; Material mat = skin.GPUmaterials[subIdx]; if (mat == null) continue; Texture tex = mat.GetTexture("_MainTex"); if (tex == null) continue; int[] tris = m.GetTriangles(subIdx); if (tris.Length == 0) continue; Texture2D readable = null; Color diffuse = mat.HasProperty("_Color") ? mat.GetColor("_Color") : Color.white; HashSet bodyVerts = new HashSet(tris); foreach (int v in bodyVerts) { if (graftVerts.Contains(v)) continue; Vector3 pos = verts[v]; if (pos.x < bbMin.x || pos.y < bbMin.y || pos.z < bbMin.z || pos.x > bbMax.x || pos.y > bbMax.y || pos.z > bbMax.z) continue; if (readable == null) { if (!readableCache.TryGetValue(tex, out readable)) { readable = ReadTextureToCPU(tex, 1024); readableCache[tex] = readable; } if (readable == null) break; } Vector2 uv = uvs[v]; Color c = readable.GetPixelBilinear(uv.x - Mathf.Floor(uv.x), uv.y - Mathf.Floor(uv.y)); smpPos.Add(pos); smpNrm.Add(hasNormals ? normals[v] : Vector3.zero); smpCol.Add(new Color(c.r * diffuse.r, c.g * diffuse.g, c.b * diffuse.b)); } } } finally { foreach (var kvp in readableCache) { if (kvp.Value != null) UnityEngine.Object.Destroy(kvp.Value); } } sampleCount = smpPos.Count; if (sampleCount == 0) return false; // 2cm spatial hash grid over the samples so the per-vertex nearest // search doesn't go all-against-all. Hash collisions only ever add // far-away candidates, which the distance checks filter out. const float cell = 0.02f; Dictionary> grid = new Dictionary>(); for (int i = 0; i < smpPos.Count; i++) { long key = GridKey(smpPos[i], cell); List lst; if (!grid.TryGetValue(key, out lst)) { lst = new List(); grid[key] = lst; } lst.Add(i); } List found = new List(); List foundDist = new List(); foreach (int v in graftVerts) { Vector3 pos = verts[v]; // +-2 cells guarantees everything within 4cm; widen to +-3 (6cm) // only when that neighborhood is empty CollectGridCandidates(grid, pos, cell, 2, found); if (found.Count == 0) CollectGridCandidates(grid, pos, cell, 3, found); int nearest = -1; float nearestDist = float.MaxValue; foundDist.Clear(); for (int fi = 0; fi < found.Count; fi++) { float d = Vector3.Distance(pos, smpPos[found[fi]]); foundDist.Add(d); if (d < nearestDist) { nearestDist = d; nearest = fi; } } if (nearest < 0 || nearestDist > HiddenSeamSearchRange) { vertW[v] = 1f; if (vertWSurf != null) vertWSurf[v] = 1f; unpainted++; continue; } int nearestSmp = found[nearest]; Vector3 nrm = smpNrm[nearestSmp]; float signedDist; if (nrm.sqrMagnitude > 0.25f) signedDist = Vector3.Dot(pos - smpPos[nearestSmp], nrm.normalized); else signedDist = nearestDist; // no normals: no burial detection // The anchor offset shifts where the fade starts: negative pulls // it into the body, positive pushes it up the shaft. signedDist -= anchorOffset; if (signedDist <= 0f) { vertW[v] = 0f; if (vertWSurf != null) vertWSurf[v] = 0f; buried++; } else { vertW[v] = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(signedDist / fadeRange)); if (vertWSurf != null) vertWSurf[v] = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(signedDist / surfRange)); faded++; } // Local body color around the exit point: inverse-square distance // weighted average of the nearby samples float colorRadius = Mathf.Max(0.02f, nearestDist + 0.01f); float sr = 0f, sg = 0f, sb = 0f, sw = 0f; for (int fi = 0; fi < found.Count; fi++) { float d = foundDist[fi]; if (d > colorRadius) continue; float wgt = 1f / (d * d + 0.00001f); Color cc = smpCol[found[fi]]; sr += cc.r * wgt; sg += cc.g * wgt; sb += cc.b * wgt; sw += wgt; } vertCol[v] = sw > 0f ? new Color(sr / sw, sg / sw, sb / sw) : fallbackColor; } return true; } // Fades the penis material's spec/gloss/normal maps toward their own // average color in a narrow band around the contact area, so the abrupt // change in shine and micro-detail stops reading as a seam. Fading toward // the map's own average removes detail without disturbing its encoding // (safe for normal maps too) and keeps the overall intensity. private void ApplyGraftSurfaceMapFade(Material penisMat, float[] surfW) { // Always bake from the pristine maps: put originals back first RestoreGraftSurfaceMaps(); if (penisMat == null) return; // Diagnostic: at the slider maximum the maps are removed outright. // If even that changes nothing on screen, this shader/material does // not actually sample these properties and the map fade is a dead end. bool nullTest = seamSurfaceRange != null && seamSurfaceRange.val >= 0.19f; _seamSurfNulled = nullTest; string[] props = new string[] { "_SpecTex", "_GlossTex", "_BumpMap" }; List done = new List(); foreach (string prop in props) { if (!penisMat.HasProperty(prop)) continue; Texture orig = penisMat.GetTexture(prop); if (orig == null) continue; if (nullTest) { _seamSurfPrev[prop] = orig; penisMat.SetTexture(prop, null); done.Add(prop); continue; } Texture2D readable = ReadTextureToCPU(orig, 1024); if (readable == null) continue; Color[] opx = readable.GetPixels(); UnityEngine.Object.Destroy(readable); if (opx == null || opx.Length != surfW.Length) continue; float ar = 0f, ag = 0f, ab = 0f, aa = 0f; for (int i = 0; i < opx.Length; i++) { ar += opx[i].r; ag += opx[i].g; ab += opx[i].b; aa += opx[i].a; } Color avg = new Color(ar / opx.Length, ag / opx.Length, ab / opx.Length, aa / opx.Length); for (int i = 0; i < opx.Length; i++) { float a = 1f - AdjustSeamWeight(surfW[i], 2f); if (a <= 0f) continue; opx[i] = Color.Lerp(opx[i], avg, a); } Texture2D baked = new Texture2D(1024, 1024, TextureFormat.ARGB32, false); baked.wrapMode = TextureWrapMode.Repeat; baked.SetPixels(opx); baked.Apply(); _seamSurfPrev[prop] = orig; _seamSurfTextures.Add(baked); penisMat.SetTexture(prop, baked); done.Add(prop); } SuperController.LogMessage($"[AF debug] surface maps {(nullTest ? "NULLED (diagnostic)" : "faded")}: {(done.Count > 0 ? string.Join(",", done.ToArray()) : "none")}, shader='{penisMat.shader.name}'"); } private void RestoreGraftSurfaceMaps() { if (_seamSurfPrev.Count > 0 && skin != null && skin.GPUmaterials != null && skin.GPUmaterials.Length > 30) { Material penisMat = skin.GPUmaterials[30]; if (penisMat != null) { foreach (var kvp in _seamSurfPrev) { if (penisMat.HasProperty(kvp.Key)) penisMat.SetTexture(kvp.Key, kvp.Value); } } } _seamSurfPrev.Clear(); foreach (var t in _seamSurfTextures) { if (t != null) UnityEngine.Object.Destroy(t); } _seamSurfTextures.Clear(); _seamSurfNulled = false; } private static long GridKey(Vector3 p, float cell) { long ix = (long)Mathf.Floor(p.x / cell); long iy = (long)Mathf.Floor(p.y / cell); long iz = (long)Mathf.Floor(p.z / cell); return (ix * 73856093L) ^ (iy * 19349663L) ^ (iz * 83492791L); } // Fills 'result' with sample indices from the (2n+1)^3 cell neighborhood // around p. A HashSet keeps colliding cells from being added twice. private static void CollectGridCandidates(Dictionary> grid, Vector3 p, float cell, int n, List result) { result.Clear(); long ix = (long)Mathf.Floor(p.x / cell); long iy = (long)Mathf.Floor(p.y / cell); long iz = (long)Mathf.Floor(p.z / cell); HashSet seen = new HashSet(); for (long dx = -n; dx <= n; dx++) { for (long dy = -n; dy <= n; dy++) { for (long dz = -n; dz <= n; dz++) { long key = ((ix + dx) * 73856093L) ^ ((iy + dy) * 19349663L) ^ ((iz + dz) * 83492791L); if (!seen.Add(key)) continue; List lst; if (grid.TryGetValue(key, out lst)) result.AddRange(lst); } } } } // Bakes a grayscale gradient for the given triangles into their material UV // space. Vertices found in vertWeights use that value; all others are white. private Texture2D BakeGradientForTris(List tris, Dictionary vertWeights, Vector2[] uvs, int texSize) { RenderTexture rt = RenderTexture.GetTemporary(texSize, texSize, 0, RenderTextureFormat.ARGB32); RenderTexture oldRT = RenderTexture.active; RenderTexture.active = rt; GL.Clear(true, true, Color.white); Material glMat = new Material(Shader.Find("Hidden/Internal-Colored")); glMat.SetPass(0); GL.PushMatrix(); GL.LoadOrtho(); GL.Begin(GL.TRIANGLES); for (int i = 0; i < tris.Count; i += 3) { int v1 = tris[i]; int v2 = tris[i + 1]; int v3 = tris[i + 2]; float w1, w2, w3; if (!vertWeights.TryGetValue(v1, out w1)) w1 = 1f; if (!vertWeights.TryGetValue(v2, out w2)) w2 = 1f; if (!vertWeights.TryGetValue(v3, out w3)) w3 = 1f; if (w1 >= 1f && w2 >= 1f && w3 >= 1f) continue; float uOffset = Mathf.Floor(uvs[v1].x); float vOffset = Mathf.Floor(uvs[v1].y); GL.Color(new Color(w1, w1, w1, 1f)); GL.Vertex3(uvs[v1].x - uOffset, uvs[v1].y - vOffset, 0f); GL.Color(new Color(w2, w2, w2, 1f)); GL.Vertex3(uvs[v2].x - uOffset, uvs[v2].y - vOffset, 0f); GL.Color(new Color(w3, w3, w3, 1f)); GL.Vertex3(uvs[v3].x - uOffset, uvs[v3].y - vOffset, 0f); } GL.End(); GL.PopMatrix(); Texture2D result = new Texture2D(texSize, texSize, TextureFormat.ARGB32, false); result.ReadPixels(new Rect(0, 0, texSize, texSize), 0, 0); result.Apply(); RenderTexture.active = oldRT; RenderTexture.ReleaseTemporary(rt); return result; } // Bakes interpolated vertex colors for the given triangles into their // material UV space. Vertices missing from vertColors use fallback, and the // target is cleared to fallback too so bilinear sampling at island edges // picks up a sane color instead of white. private Texture2D BakeColorsForTris(List tris, Dictionary vertColors, Vector2[] uvs, int texSize, Color fallback) { RenderTexture rt = RenderTexture.GetTemporary(texSize, texSize, 0, RenderTextureFormat.ARGB32); RenderTexture oldRT = RenderTexture.active; RenderTexture.active = rt; GL.Clear(true, true, new Color(fallback.r, fallback.g, fallback.b, 1f)); Material glMat = new Material(Shader.Find("Hidden/Internal-Colored")); glMat.SetPass(0); GL.PushMatrix(); GL.LoadOrtho(); GL.Begin(GL.TRIANGLES); for (int i = 0; i < tris.Count; i += 3) { int v1 = tris[i]; int v2 = tris[i + 1]; int v3 = tris[i + 2]; Color c1, c2, c3; if (!vertColors.TryGetValue(v1, out c1)) c1 = fallback; if (!vertColors.TryGetValue(v2, out c2)) c2 = fallback; if (!vertColors.TryGetValue(v3, out c3)) c3 = fallback; float uOffset = Mathf.Floor(uvs[v1].x); float vOffset = Mathf.Floor(uvs[v1].y); GL.Color(new Color(c1.r, c1.g, c1.b, 1f)); GL.Vertex3(uvs[v1].x - uOffset, uvs[v1].y - vOffset, 0f); GL.Color(new Color(c2.r, c2.g, c2.b, 1f)); GL.Vertex3(uvs[v2].x - uOffset, uvs[v2].y - vOffset, 0f); GL.Color(new Color(c3.r, c3.g, c3.b, 1f)); GL.Vertex3(uvs[v3].x - uOffset, uvs[v3].y - vOffset, 0f); } GL.End(); GL.PopMatrix(); Texture2D result = new Texture2D(texSize, texSize, TextureFormat.ARGB32, false); result.ReadPixels(new Rect(0, 0, texSize, texSize), 0, 0); result.Apply(); RenderTexture.active = oldRT; RenderTexture.ReleaseTemporary(rt); return result; } private void RequestSeamRebake() { _seamRebakeAt = Time.unscaledTime + 0.4f; _seamRebakeRetries = 0; } // Textures load asynchronously in VaM, so one that finishes loading after // our bake (e.g. the user's own decal, right after a skin reload) silently // replaces our seam decal and the blend disappears while the toggle still // says on. Detect that, adopt the new texture as the original to restore, // and re-apply. private void CheckSeamDecalIntegrity() { if (!_seamDecalApplied || _seamColorDecalTex == null) return; if (skin == null || skin.GPUmaterials == null) return; bool dirty = false; Material pelvisMat = skin.GPUmaterials.Length > 29 ? skin.GPUmaterials[29] : null; Material penisMat = skin.GPUmaterials.Length > 30 ? skin.GPUmaterials[30] : null; if (penisMat != null && penisMat.HasProperty("_DecalTex") && !ReferenceEquals(penisMat.GetTexture("_DecalTex"), _seamColorDecalTex)) { _seamDecalPrevPenis = penisMat.GetTexture("_DecalTex"); dirty = true; } if (pelvisMat != null && pelvisMat.HasProperty("_DecalTex") && !ReferenceEquals(pelvisMat.GetTexture("_DecalTex"), _seamColorDecalTex)) { _seamDecalPrevPelvis = pelvisMat.GetTexture("_DecalTex"); dirty = true; } // Also watch the surface maps: if something (material options, async // texture loads) re-applies its own spec/gloss/normal textures, our // fade silently disappears. Adopt the new texture and re-bake. if (penisMat != null && _seamSurfPrev.Count > 0) { List replaced = null; foreach (var kvp in _seamSurfPrev) { if (!penisMat.HasProperty(kvp.Key)) continue; Texture cur = penisMat.GetTexture(kvp.Key); bool stillOurs = _seamSurfNulled ? cur == null : _seamSurfTextures.Contains(cur as Texture2D); if (!stillOurs) { if (replaced == null) replaced = new List(); replaced.Add(kvp.Key); } } if (replaced != null) { foreach (string prop in replaced) { _seamSurfPrev[prop] = penisMat.GetTexture(prop); } SuperController.LogMessage($"[AF debug] surface maps externally replaced: {string.Join(",", replaced.ToArray())} - rebaking"); dirty = true; } } List changedBodyMats = null; foreach (var kvp in _bodySeamDecalPrev) { Material mat = kvp.Key; if (mat == null) continue; Texture cur = mat.GetTexture("_DecalTex"); if (!_bodySeamDecalTextures.Contains(cur as Texture2D)) { if (changedBodyMats == null) changedBodyMats = new List(); changedBodyMats.Add(mat); } } if (changedBodyMats != null) { foreach (var mat in changedBodyMats) { _bodySeamDecalPrev[mat] = mat.GetTexture("_DecalTex"); } dirty = true; } if (dirty) RequestSeamRebake(); } // Undoes the seam blend and drops all bookkeeping. Called while the old // skin still exists, right before it is unloaded/rebuilt: the graft // materials get rebuilt anyway, but the BODY materials survive the reload, // so their original decals must be put back here — otherwise they keep // pointing at our destroyed decal textures and render a gray band. private void ResetSeamBlendState() { if (_seamDecalApplied) { if (skin != null && skin.GPUmaterials != null) { Material pelvisMat = skin.GPUmaterials.Length > 29 ? skin.GPUmaterials[29] : null; Material penisMat = skin.GPUmaterials.Length > 30 ? skin.GPUmaterials[30] : null; if (pelvisMat != null && pelvisMat.HasProperty("_DecalTex")) pelvisMat.SetTexture("_DecalTex", _seamDecalPrevPelvis); if (penisMat != null && penisMat.HasProperty("_DecalTex")) penisMat.SetTexture("_DecalTex", _seamDecalPrevPenis); } _seamDecalApplied = false; } _seamDecalPrevPelvis = null; _seamDecalPrevPenis = null; RestoreBodySeamDecals(); RestoreGraftSurfaceMaps(); if (_seamColorDecalTex != null) { UnityEngine.Object.Destroy(_seamColorDecalTex); _seamColorDecalTex = null; } // The rebuilt skin comes back without the blend, so uncheck the toggle // (and drop any pending rebake) so the UI matches reality; re-enable // the toggle to bake again on the new skin. _seamRebakeAt = -1f; _skinColorMatched = false; if (seamColorBlendEnabled != null) seamColorBlendEnabled.valNoCallback = false; } // Pushes the blend outward: higher powers keep the decal near-opaque across // most of the radius (a wide soft repaint patch), lower powers give a real // gradient from the seam. The two blend modes use different powers. private float AdjustSeamWeight(float w, float power) { return Mathf.Pow(Mathf.Clamp01(w), power); } // Expands (dilates) the dark decal regions of a baked gradient by a few texels // so bilinear filtering at UV island edges doesn't fade the decal right at the // seam, which would let dark unpainted texture regions show through as a line. private void DilateGradient(Color[] px, int size, int passes) { float[] cur = new float[px.Length]; for (int i = 0; i < px.Length; i++) cur[i] = px[i].r; float[] tmp = new float[px.Length]; for (int p = 0; p < passes; p++) { for (int y = 0; y < size; y++) { int row = y * size; for (int x = 0; x < size; x++) { float v = cur[row + x]; if (x > 0 && cur[row + x - 1] < v) v = cur[row + x - 1]; if (x < size - 1 && cur[row + x + 1] < v) v = cur[row + x + 1]; tmp[row + x] = v; } } for (int y = 0; y < size; y++) { int row = y * size; for (int x = 0; x < size; x++) { float v = tmp[row + x]; if (y > 0 && tmp[row + x - size] < v) v = tmp[row + x - size]; if (y < size - 1 && tmp[row + x + size] < v) v = tmp[row + x + size]; cur[row + x] = v; } } } for (int i = 0; i < px.Length; i++) { Color c = px[i]; c.r = cur[i]; px[i] = c; } } private void RestoreBodySeamDecals() { foreach (var kvp in _bodySeamDecalPrev) { if (kvp.Key != null && kvp.Key.HasProperty("_DecalTex")) kvp.Key.SetTexture("_DecalTex", kvp.Value); } _bodySeamDecalPrev.Clear(); foreach (var t in _bodySeamDecalTextures) { if (t != null) UnityEngine.Object.Destroy(t); } _bodySeamDecalTextures.Clear(); } // Fades the body's own texture detail toward the seam color on the body side // of the connection, mirroring the graft-side decal. Composites over any // existing decal so user decals are preserved and restored at radius 0. // Only used with the pelvis visible: there the seam runs through evenly // colored skin and this wide soft patch is what makes it disappear. private void ApplyBodySeamDecals(float fadeRadius, Color seamColor) { Mesh m = GetSkinMesh(); if (m == null) return; Vector3[] verts = m.vertices; Vector2[] uvs = m.uv; if (verts == null || verts.Length == 0 || uvs == null || uvs.Length < verts.Length) return; int[] graftSubmeshes; if (hideMalePelvis != null && hideMalePelvis.val) graftSubmeshes = new int[] { 30 }; else graftSubmeshes = new int[] { 29, 30 }; List graftTris = new List(); foreach (int subIdx in graftSubmeshes) { if (subIdx < m.subMeshCount) graftTris.AddRange(m.GetTriangles(subIdx)); } if (graftTris.Count == 0) return; HashSet seamVerts = FindSeamVerts(m, graftTris, verts); if (seamVerts.Count == 0) return; Vector3 bbMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); Vector3 bbMax = new Vector3(float.MinValue, float.MinValue, float.MinValue); foreach (int sv in seamVerts) { Vector3 p = verts[sv]; bbMin = Vector3.Min(bbMin, p); bbMax = Vector3.Max(bbMax, p); } bbMin -= new Vector3(fadeRadius, fadeRadius, fadeRadius); bbMax += new Vector3(fadeRadius, fadeRadius, fadeRadius); // Group body submeshes by material instance so shared materials get one decal HashSet graftVertSet = new HashSet(graftTris); Dictionary> matTris = new Dictionary>(); for (int subIdx = 0; subIdx < m.subMeshCount; subIdx++) { if (subIdx == 29 || subIdx == 30) continue; if (subIdx >= skin.GPUmaterials.Length) continue; if (skin.materialsEnabled != null && subIdx < skin.materialsEnabled.Length && !skin.materialsEnabled[subIdx]) continue; Material mat = skin.GPUmaterials[subIdx]; if (mat == null || !mat.HasProperty("_DecalTex")) continue; int[] tris = m.GetTriangles(subIdx); if (tris.Length == 0) continue; List lst; if (!matTris.TryGetValue(mat, out lst)) { lst = new List(); matTris[mat] = lst; } lst.AddRange(tris); } // Radius may have changed: put original decals back before re-applying RestoreBodySeamDecals(); foreach (var entry in matTris) { Material mat = entry.Key; List tris = entry.Value; // Distance-based weight for body verts near the seam ring Dictionary weights = new Dictionary(); HashSet vertSet = new HashSet(tris); bool anyNear = false; foreach (int v in vertSet) { if (graftVertSet.Contains(v)) continue; Vector3 pos = verts[v]; if (pos.x < bbMin.x || pos.y < bbMin.y || pos.z < bbMin.z || pos.x > bbMax.x || pos.y > bbMax.y || pos.z > bbMax.z) continue; float minDist = float.MaxValue; foreach (int sv in seamVerts) { float dist = Vector3.Distance(pos, verts[sv]); if (dist < minDist) minDist = dist; } if (minDist > fadeRadius) continue; weights[v] = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(minDist / fadeRadius)); anyNear = true; } if (!anyNear) continue; Texture2D gradient = BakeGradientForTris(tris, weights, uvs, 1024); if (gradient == null) continue; // Composite our fade over any existing decal Texture prevDecal = mat.GetTexture("_DecalTex"); Texture2D prevReadable = prevDecal != null ? ReadTextureToCPU(prevDecal, 1024) : null; Color[] px = gradient.GetPixels(); Color[] prevPx = prevReadable != null ? prevReadable.GetPixels() : null; for (int i = 0; i < px.Length; i++) { float a = 1f - px[i].r; if (prevPx != null) { Color o = prevPx[i]; float outA = a + o.a * (1f - a); if (outA > 0.0001f) { px[i] = new Color( (seamColor.r * a + o.r * o.a * (1f - a)) / outA, (seamColor.g * a + o.g * o.a * (1f - a)) / outA, (seamColor.b * a + o.b * o.a * (1f - a)) / outA, outA); } else { px[i] = new Color(0f, 0f, 0f, 0f); } } else { px[i] = new Color(seamColor.r, seamColor.g, seamColor.b, a); } } UnityEngine.Object.Destroy(gradient); if (prevReadable != null) UnityEngine.Object.Destroy(prevReadable); Texture2D decal = new Texture2D(1024, 1024, TextureFormat.ARGB32, false); decal.wrapMode = TextureWrapMode.Repeat; decal.SetPixels(px); decal.Apply(); _bodySeamDecalPrev[mat] = prevDecal; _bodySeamDecalTextures.Add(decal); mat.SetTexture("_DecalTex", decal); if (mat.HasProperty("_DecalAlpha")) mat.SetFloat("_DecalAlpha", 1f); } } // Matches the penis diffuse color to the body. Returns false when the // skin/materials are not ready yet so the caller can retry. private bool MatchSkinColor() { if (skin == null || skin.GPUmaterials == null) return false; Material torsoMat = null; foreach (var m in skin.GPUmaterials) { if (m != null && m.name.IndexOf("Torso", StringComparison.OrdinalIgnoreCase) >= 0) { torsoMat = m; break; } } if (torsoMat == null && skin.GPUmaterials.Length > 2) { torsoMat = skin.GPUmaterials[2]; } if (torsoMat == null) return false; Material penisMat = skin.GPUmaterials.Length > 30 ? skin.GPUmaterials[30] : null; if (penisMat == null) return false; Texture torsoTex = torsoMat.GetTexture("_MainTex"); Texture penisTex = penisMat.GetTexture("_MainTex"); if (torsoTex == null || penisTex == null) return false; Color torsoTexAvg = GetAverageColor(torsoTex); Color penisTexAvg = GetAverageColor(penisTex); Color torsoDiffuse = Color.white; if (torsoMat.HasProperty("_Color")) { torsoDiffuse = torsoMat.GetColor("_Color"); } // Target final color is Torso Texture Avg * Torso Diffuse Color targetFinalColor = new Color( torsoTexAvg.r * torsoDiffuse.r, torsoTexAvg.g * torsoDiffuse.g, torsoTexAvg.b * torsoDiffuse.b, 1f ); // We want: Penis Texture Avg * Penis Diffuse = Target Final Color // So: Penis Diffuse = Target Final Color / Penis Texture Avg Color penisDiffuse = new Color( penisTexAvg.r > 0.01f ? Mathf.Clamp01(targetFinalColor.r / penisTexAvg.r) : 1f, penisTexAvg.g > 0.01f ? Mathf.Clamp01(targetFinalColor.g / penisTexAvg.g) : 1f, penisTexAvg.b > 0.01f ? Mathf.Clamp01(targetFinalColor.b / penisTexAvg.b) : 1f, 1f ); if (genMaterialOptions == null) return false; // Sync all other parameters (Specular, Gloss, etc.) first var skinMaterialOpts = containingAtom.GetStorableByID("skin") as DAZCharacterMaterialOptions; if (skinMaterialOpts != null) { genMaterialOptions.CopyMaterialParams(skinMaterialOpts); } var diffuseStorable = genMaterialOptions.GetColorJSONParam("Diffuse Color"); if (diffuseStorable == null) return false; // Remember the color the user had, so the toggle can restore it if (!_skinColorMatched) { _skinColorPrev = diffuseStorable.val; _skinColorMatched = true; } float H, S, V; Color.RGBToHSV(penisDiffuse, out H, out S, out V); var currentHSV = diffuseStorable.val; currentHSV.H = H; currentHSV.S = S; currentHSV.V = V; diffuseStorable.val = currentHSV; return true; } private void RestoreSkinColor() { if (!_skinColorMatched) return; _skinColorMatched = false; if (genMaterialOptions == null) return; var diffuseStorable = genMaterialOptions.GetColorJSONParam("Diffuse Color"); if (diffuseStorable != null) diffuseStorable.val = _skinColorPrev; } } } namespace Skynet { // =========================================================================================== // ToolTipManager - static manager that owns the shared tooltip panel and tracks all triggers. // =========================================================================================== public static class ToolTipManager { private static GameObject _tooltipPanel; private static Text _tooltipText; private static Image _tooltipBackground; private static Canvas _tooltipCanvas; private static RectTransform _tooltipRect; private static CanvasGroup _tooltipCanvasGroup; private static List _activeTriggers; private static MVRScript _owner; private static bool _initialized; private static Sprite _roundedSprite; // Appearance (internal, not modifiable) private const float Scale = 0.7f; private static readonly Color BgColor = new Color(0.835f, 0.835f, 0.835f, 0.97f); private static readonly Color TextColor = new Color(0.05f, 0.05f, 0.05f, 1f); private static readonly Color BorderColor = new Color(0.608f, 0.494f, 0.667f, 1f); private const int FontSize = 22; private const float MaxWidth = 400f; private const float Padding = 10f; private const float OffsetX = 15f; private const float OffsetY = -15f; private const float FadeSpeed = 8f; private static float _showDelay = 0.35f; private const int CornerRadius = 6; private const int BorderPx = 2; private static bool _enabled = true; private static JSONStorableBool _enabledStorable; // Crossfade state private static float _hoverTimer; private static float _currentAlpha; private static bool _isShowing; private static string _currentText; private static bool _isCrossfading; private static string _pendingText; private static ToolTipTrigger _currentTrigger; private static ToolTipTrigger _pendingTrigger; // --------------------------------------------------------------- // Public API // --------------------------------------------------------------- public static void Init(MVRScript owner) { _owner = owner; if (_activeTriggers == null) _activeTriggers = new List(); _activeTriggers.Clear(); _initialized = true; _isCrossfading = false; _enabled = true; _enabledStorable = null; _currentTrigger = null; _pendingTrigger = null; CreateTooltipPanel(); } // Destroy tooltip canvas & triggers (call in plugin OnDestroy() method) public static void Destroy() { if (_activeTriggers != null) { for (int i = 0; i < _activeTriggers.Count; i++) { if (_activeTriggers[i] != null) UnityEngine.Object.Destroy(_activeTriggers[i]); } _activeTriggers.Clear(); } if (_tooltipPanel != null) { UnityEngine.Object.Destroy(_tooltipPanel); _tooltipPanel = null; } if (_tooltipCanvas != null) { UnityEngine.Object.Destroy(_tooltipCanvas.gameObject); _tooltipCanvas = null; } if (_roundedSprite != null) { if (_roundedSprite.texture != null) UnityEngine.Object.Destroy(_roundedSprite.texture); UnityEngine.Object.Destroy(_roundedSprite); _roundedSprite = null; } _initialized = false; _isShowing = false; _isCrossfading = false; _currentAlpha = 0f; _enabled = true; _enabledStorable = null; _currentTrigger = null; _pendingTrigger = null; _owner = null; } // --------------------------------------------------------------- // AddTooltip - one overload per UI type, just pass the text string // --------------------------------------------------------------- public static void AddTooltip(UIDynamic uiElement, string tooltipText) { if (!_initialized || uiElement == null) return; AddTrigger(uiElement.transform, tooltipText); } public static void AddTooltip(UIDynamicButton button, string tooltipText) { if (!_initialized || button == null) return; AddTrigger(button.transform, tooltipText); } public static void AddTooltip(UIDynamicSlider slider, string tooltipText) { if (!_initialized || slider == null) return; AddTrigger(slider.transform, tooltipText); } public static void AddTooltip(UIDynamicToggle toggle, string tooltipText) { if (!_initialized || toggle == null) return; AddTrigger(toggle.transform, tooltipText); } public static void AddTooltip(UIDynamicPopup popup, string tooltipText) { if (!_initialized || popup == null) return; AddTrigger(popup.transform, tooltipText); } public static void AddTooltip(UIDynamicTextField textfield, string tooltipText) { if (!_initialized || textfield == null) return; AddTrigger(textfield.transform, tooltipText); } public static void AddTooltip(UIDynamicColorPicker colorPicker, string tooltipText) { if (!_initialized || colorPicker == null) return; AddTrigger(colorPicker.transform, tooltipText); } public static void AddTooltip(Transform target, string tooltipText) { if (!_initialized || target == null) return; AddTrigger(target, tooltipText); } // Creates an enable/disable toggle in the plugin UI public static void CreateToggle(MVRScript plugin, bool rightSide = false) { if (plugin == null) return; if (_enabledStorable == null) { _enabledStorable = new JSONStorableBool("Show Tooltips", true, OnEnabledChanged); _enabledStorable.storeType = JSONStorableParam.StoreType.Full; plugin.RegisterBool(_enabledStorable); } plugin.CreateToggle(_enabledStorable, rightSide); } // Enable or disable tooltips from external code public static void SetEnabled(bool enabled) { _enabled = enabled; if (!_enabled) HideTooltip(); if (_enabledStorable != null && _enabledStorable.val != enabled) _enabledStorable.valNoCallback = enabled; } // Set the hover delay in seconds before tooltip appears (default 0.35) public static void SetShowDelay(float seconds) { _showDelay = Mathf.Max(0f, seconds); } private static void OnEnabledChanged(bool val) { _enabled = val; if (!_enabled) HideTooltip(); } // Suspend tooltip system without destroying it (call in plugin OnDisable method) public static void Suspend() { if (!_initialized) return; HideTooltip(); if (_tooltipPanel != null) _tooltipPanel.SetActive(false); if (_tooltipCanvas != null) { ToolTipUpdater updater = _tooltipCanvas.GetComponent(); if (updater != null) updater.enabled = false; } } // Resume tooltip system after Suspend (call in plugin OnEnable method) public static void Resume() { if (!_initialized) return; if (_tooltipCanvas != null) { ToolTipUpdater updater = _tooltipCanvas.GetComponent(); if (updater != null) updater.enabled = true; } } // Remove all tooltips (call before rebuilding UI) public static void RemoveAllTooltips() { if (_activeTriggers == null) return; for (int i = 0; i < _activeTriggers.Count; i++) { if (_activeTriggers[i] != null) UnityEngine.Object.Destroy(_activeTriggers[i]); } _activeTriggers.Clear(); HideTooltip(); } // --------------------------------------------------------------- // Internal - called by ToolTipTrigger // --------------------------------------------------------------- internal static void ShowTooltip(ToolTipTrigger trigger, string text) { if (!_enabled) return; _currentTrigger = trigger; if (_currentText == text && _isShowing && !_isCrossfading) return; if (_currentAlpha > 0.01f && _currentText != text) { _isCrossfading = true; _pendingText = text; _pendingTrigger = trigger; _isShowing = false; _hoverTimer = 0f; return; } _currentText = text; _isShowing = true; _isCrossfading = false; _hoverTimer = 0f; _pendingTrigger = null; } internal static void HideTooltip() { HideTooltip(null); } internal static void HideTooltip(ToolTipTrigger trigger) { if (trigger != null && trigger != _currentTrigger) return; _currentTrigger = null; _pendingTrigger = null; _isShowing = false; _isCrossfading = false; _hoverTimer = 0f; } internal static void UpdateTooltipPosition() { if (_tooltipPanel == null || _tooltipCanvas == null) return; float dt = Time.unscaledDeltaTime; if (_isCrossfading) { _currentAlpha = Mathf.MoveTowards(_currentAlpha, 0f, dt * FadeSpeed * 2f); if (_currentAlpha <= 0.01f) { _currentText = _pendingText; _isCrossfading = false; _currentAlpha = 0f; _hoverTimer = 0f; _isShowing = true; _currentTrigger = _pendingTrigger; _pendingTrigger = null; if (_tooltipText != null) _tooltipText.text = _currentText; } } else if (_isShowing) { _hoverTimer += dt; if (_hoverTimer >= _showDelay) _currentAlpha = Mathf.MoveTowards(_currentAlpha, 1f, dt * FadeSpeed); } else { _currentAlpha = Mathf.MoveTowards(_currentAlpha, 0f, dt * FadeSpeed * 2f); } if (_tooltipCanvasGroup != null) _tooltipCanvasGroup.alpha = _currentAlpha; bool visible = _currentAlpha > 0.01f; if (_tooltipPanel.activeSelf != visible) _tooltipPanel.SetActive(visible); if (!visible) return; if (_tooltipText != null && _tooltipText.text != _currentText) _tooltipText.text = _currentText; Vector3 mousePos = Input.mousePosition; float panelWidth = _tooltipRect.rect.width * Scale; float panelHeight = _tooltipRect.rect.height * Scale; float x = mousePos.x + OffsetX; float y = mousePos.y + OffsetY; if (x + panelWidth > Screen.width) x = mousePos.x - OffsetX - panelWidth; if (y - panelHeight < 0) y = mousePos.y - OffsetY; if (x < 0) x = 0; if (y > Screen.height) y = Screen.height; _tooltipRect.position = new Vector3(x, y, 0f); } // --------------------------------------------------------------- // Private helpers // --------------------------------------------------------------- private static Sprite CreateRoundedRectSprite(int width, int height, int radius, Color fill, Color border, int borderPx) { Texture2D tex = new Texture2D(width, height, TextureFormat.ARGB32, false); tex.filterMode = FilterMode.Bilinear; Color clear = new Color(0f, 0f, 0f, 0f); Color[] pixels = new Color[width * height]; for (int i = 0; i < pixels.Length; i++) pixels[i] = clear; for (int py = 0; py < height; py++) { for (int px = 0; px < width; px++) { float dx = 0f; float dy = 0f; bool inCorner = false; if (px < radius && py < radius) { dx = radius - px - 0.5f; dy = radius - py - 0.5f; inCorner = true; } else if (px >= width - radius && py < radius) { dx = px - (width - radius) + 0.5f; dy = radius - py - 0.5f; inCorner = true; } else if (px < radius && py >= height - radius) { dx = radius - px - 0.5f; dy = py - (height - radius) + 0.5f; inCorner = true; } else if (px >= width - radius && py >= height - radius) { dx = px - (width - radius) + 0.5f; dy = py - (height - radius) + 0.5f; inCorner = true; } if (inCorner) { float dist = Mathf.Sqrt(dx * dx + dy * dy); if (dist > radius) continue; if (dist > radius - 1f) { float a = radius - dist; pixels[py * width + px] = new Color(border.r, border.g, border.b, border.a * a); } else if (dist > radius - borderPx) pixels[py * width + px] = border; else pixels[py * width + px] = fill; } else { bool isBorder = px < borderPx || px >= width - borderPx || py < borderPx || py >= height - borderPx; pixels[py * width + px] = isBorder ? border : fill; } } } tex.SetPixels(pixels); tex.Apply(false, false); Vector4 sliceBorder = new Vector4(radius, radius, radius, radius); return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect, sliceBorder); } private static void CreateTooltipPanel() { if (_tooltipCanvas == null) { var canvasGO = new GameObject("SkynetTooltipCanvas"); _tooltipCanvas = canvasGO.AddComponent(); _tooltipCanvas.renderMode = RenderMode.ScreenSpaceOverlay; _tooltipCanvas.sortingOrder = 10000; canvasGO.AddComponent(); canvasGO.AddComponent().enabled = false; canvasGO.AddComponent(); } if (_tooltipPanel == null) { int spriteSize = CornerRadius * 3 + 2; _roundedSprite = CreateRoundedRectSprite(spriteSize, spriteSize, CornerRadius, BgColor, BorderColor, BorderPx); _tooltipPanel = new GameObject("TooltipPanel"); _tooltipPanel.transform.SetParent(_tooltipCanvas.transform, false); _tooltipRect = _tooltipPanel.AddComponent(); _tooltipRect.pivot = new Vector2(0f, 1f); _tooltipRect.localScale = new Vector3(Scale, Scale, 1f); _tooltipCanvasGroup = _tooltipPanel.AddComponent(); _tooltipCanvasGroup.alpha = 0f; _tooltipCanvasGroup.blocksRaycasts = false; _tooltipCanvasGroup.interactable = false; _tooltipBackground = _tooltipPanel.AddComponent(); _tooltipBackground.sprite = _roundedSprite; _tooltipBackground.type = Image.Type.Sliced; _tooltipBackground.color = Color.white; int pad = Mathf.RoundToInt(Padding); var hlg = _tooltipPanel.AddComponent(); hlg.padding = new RectOffset(pad + BorderPx, pad + BorderPx, pad + BorderPx, pad + BorderPx); hlg.childForceExpandWidth = false; hlg.childForceExpandHeight = false; var csf = _tooltipPanel.AddComponent(); csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize; var textGO = new GameObject("TooltipText"); textGO.transform.SetParent(_tooltipPanel.transform, false); _tooltipText = textGO.AddComponent(); _tooltipText.font = Resources.GetBuiltinResource("Arial.ttf"); _tooltipText.fontSize = FontSize; _tooltipText.color = TextColor; _tooltipText.alignment = TextAnchor.UpperLeft; _tooltipText.horizontalOverflow = HorizontalWrapMode.Wrap; _tooltipText.verticalOverflow = VerticalWrapMode.Overflow; _tooltipText.raycastTarget = false; _tooltipText.text = ""; var textLE = textGO.AddComponent(); textLE.preferredWidth = MaxWidth; _tooltipPanel.SetActive(false); } } private static void AddTrigger(Transform target, string text) { Graphic graphic = target.GetComponent(); if (graphic == null) { Image img = target.gameObject.AddComponent(); img.color = new Color(0f, 0f, 0f, 0f); img.raycastTarget = true; } else { graphic.raycastTarget = true; } ToolTipTrigger trigger = target.gameObject.GetComponent(); if (trigger == null) trigger = target.gameObject.AddComponent(); trigger.tooltipText = text; if (!_activeTriggers.Contains(trigger)) _activeTriggers.Add(trigger); } } // =========================================================================================== // ToolTipTrigger - attached to each UI element. Fires show/hide on pointer enter/exit. // =========================================================================================== public class ToolTipTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { public string tooltipText; public void OnPointerEnter(PointerEventData eventData) { if (!string.IsNullOrEmpty(tooltipText)) ToolTipManager.ShowTooltip(this, tooltipText); } public void OnPointerExit(PointerEventData eventData) { ToolTipManager.HideTooltip(this); } private void OnDisable() { ToolTipManager.HideTooltip(this); } private void OnDestroy() { ToolTipManager.HideTooltip(this); } } // =========================================================================================== // ToolTipUpdater - drives tooltip position/fade each frame from the canvas GameObject. // =========================================================================================== public class ToolTipUpdater : MonoBehaviour { private void Update() { ToolTipManager.UpdateTooltipPosition(); } } }