using UnityEngine; using UnityEngine.UI; using System; using System.Collections.Generic; using System.Globalization; using System.Text; using MeshVR; using MVR.FileManagementSecure; using System.Linq; using SimpleJSON; namespace Slimy { public class InstantMorph : MVRScript { class PairList : IEnumerable> { private readonly List> _items = new List>(); public void Add(int a, int b) { _items.Add(new KeyValuePair(a, b)); } // ← {a,b} を受ける public int Count { get { return _items.Count; } } public IEnumerator> GetEnumerator() { return _items.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } private JSONStorableString genParentUrl = new JSONStorableString( "Gen Parent URL", "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" ); //============================================================== // Save/Loadパラメータ //============================================================== private string _plugindir = "Saves/PluginData"; private string _pluginowner = "/Slimy"; private string _plugin = "/InstantMorph"; private string _dataFile = "savedata.json"; private string savePath = "Saves/PluginData/Slimy/InstantMorph/"; private string lastBrowseDir = "Saves/PluginData/Slimy/"; // ダミーパスでリスト更新 private string fileExt = "json"; private string ext = "json"; DAZMergedSkinV2 skin; private JSONStorableBool genSelectOnly; Vector3[] baseVertices; private Vector3[] baseNormals; private Vector3[] meshNormals; private JSONStorableBool hoverDisplayJSON; private Vector3[] currentBaseVertices; Vector3[] dragDelta; private Vector3 _lastSliderOffset = Vector3.zero; private int pendingReset = -1; int dragIndex = -1; bool dragging = false; float dragStartDepth; Vector3[] originalVertices; // 初期位置保持用 private GameObject dragGhost = null; // ドラッグ中にマウス追従させるゴースト private LineRenderer dragLine; // --- 新增:用于笔刷拖拽的临时基准(避免修改 originalVertices) --- private Vector3[] sculptBaseVertices; private InputField _editableTextField; private List matchedMorphs = new List(); private List morphSliders = new List(); // === OPT: perf/throttle fields === float __lastSurfaceBuildTime = 0f; const float __SURFACE_BUILD_INTERVAL = 0.10f; // seconds bool __prevAlt = false, __prevCtrl = false; int _lastPaintVertex = -1; bool _lastAlt = false; bool _lastCtrl = false; JSONStorableStringChooser depthModeJSON; bool IsThrough() { return depthModeJSON != null && depthModeJSON.val == "Through"; } JSONStorableString textJSON; UIDynamicTextField textField; private float _globalMax = 1f; private InputField _maxInput; MeshCollider meshCollider; GameObject meshColliderContainer; GameObject markerRoot; GameObject hoverMarker; GameObject paintCircle; int[] meshTriangles; Vector3[] meshVertices; // rawVerts (markerRoot local space) HashSet selectedVertices = new HashSet(); Dictionary vertexMarkers = new Dictionary(); int lastHovered = -1; Dictionary baseMarkers = new Dictionary(); JSONStorableFloat brushRadiusSlider; JSONStorableFloat exportScaleSlider; JSONStorableBool originalVerticesJSON; // Export scale (applies to DSF deltas only) Vector3 lastHitPoint; Vector3 lastHitNormal; bool hasLastHit = false; //const int FIXED_VERTEX_COUNT = 21556; // DSF vertex_count(Female) const int FIXED_VERTEX_COUNT = 21344; // DSF vertex_count(Male) // === 平行移動値 === float _xVal = 0f; float _yVal = 0f; float _zVal = 0f; float _xOffset = 0f; float _yOffset = 0f; float _zOffset = 0f; JSONStorableFloat xOffsetSlider; JSONStorableFloat yOffsetSlider; JSONStorableFloat zOffsetSlider; // --- 新增:拖拽笔刷跟随(Follow brush)参数 --- JSONStorableFloat followRangeSlider; // Follow Range(局部/T-pose 单位) JSONStorableFloat followWeightSlider; // Follow Weight(0..1) // 内部缓存值(实时读取 slider.val 更慢,缓存更快) private float _followRange = 0.05f; // 默认 5cm(你可根据需要调整) private float _followWeight = 0.5f; // 默认权重 0.5(50%) // === スケール値 === float _sxVal = 1f, _syVal = 1f, _szVal = 1f; JSONStorableFloat xScaleSlider; JSONStorableFloat yScaleSlider; JSONStorableFloat zScaleSlider; // === 回転値 === float _xRot = 0f, _yRot = 0f, _zRot = 0f; JSONStorableFloat xRotateSlider; JSONStorableFloat yRotateSlider; JSONStorableFloat zRotateSlider; const float SYM_EPS = 1e-3f;//3fで普通2fだと緩い private static int Quantize(float v) { return Mathf.RoundToInt(v / SYM_EPS); } private JSONStorableBool autoSymmetryDSF; // 新增:当 autoSymmetryDSF 打开时,是否把对称 DSF 拆分为左右两个文件 private JSONStorableBool splitSymmetryDSF; private Dictionary symMapBody; private Dictionary symMapGen; private void BuildSymMaps() { int gender = GetGenderInt(containingAtom); if (gender == 1) { SuperController.LogMessage("[InstantMorph] BuildSymMaps(): MALE detected"); symMapBody = SymPairsBody_male.Build(); symMapGen = SymPairsGen_male.Build(); SuperController.LogMessage("[InstantMorph] Loaded male SymPairs (body/gen)"); //return; } if (gender == 0) { SuperController.LogMessage("[InstantMorph] BuildSymMaps(): FEMALE detected"); symMapBody = SymPairsBody_female.Build(); symMapGen = SymPairsGen_female.Build(); SuperController.LogMessage("[InstantMorph] Loaded female SymPairs (body/gen)"); //return; } SuperController.LogMessage("[DEBUG] symMapBody.Count=" + (symMapBody == null ? -1 : symMapBody.Count)); //SuperController.LogError("[InstantMorph] BuildSymMaps(): gender detection failed."); } ////////////////////////////////////////////////////////////////////// public override void Init() { // --- gender-detection and SymPairs load --- int gender = GetGenderInt(containingAtom); // 0:female, 1:male, -1:unknown if (gender == 1) { SuperController.LogMessage("[InstantMorph] Detected gender: MALE"); try { SymPairsBody_male.Build(); SymPairsGen_male.Build(); } catch (System.Exception ex) { SuperController.LogError("[InstantMorph] Failed to load male SymPairs: " + ex.Message); } } else if (gender == 0) { SuperController.LogMessage("[InstantMorph] Detected gender: FEMALE"); try { SymPairsBody_female.Build(); SymPairsGen_female.Build(); } catch (System.Exception ex) { SuperController.LogError("[InstantMorph] Failed to load female SymPairs: " + ex.Message); } } else { SuperController.LogError("[InstantMorph] Gender detection failed; SymPairs not loaded."); } // --- end gender load --- string imgFilePath = GetPluginPath(this) + "/img/"; BuildSymMaps(); /* var purgeBtn = CreateButton("testButton", true); purgeBtn.button.onClick.AddListener(() => { LogSelectedVertexPositions_TPose(); //LogSelectedVertexPositions_Current(); }); */ //右カラム////////////////////////////////////// var row4 = CreateSpacer(true); row4.height =54f; var hlg4 = row4.gameObject.AddComponent(); hlg4.spacing = 0f; hlg4.childAlignment = TextAnchor.UpperCenter; hlg4.childForceExpandWidth = false; hlg4.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row4.transform, imgFilePath + "saveAll.png", () => { SaveAllSlots(); }); AddSaveButton(row4.transform, imgFilePath + "loadAll.png", () => { LoadAllSlots(); }); ////////////////////////////////////////////////////////////////// // ===== ボタン行 ===== var row = CreateSpacer(true); row.height =30f; var hlg = row.gameObject.AddComponent(); hlg.spacing = 10f; hlg.childAlignment = TextAnchor.UpperCenter; hlg.childForceExpandWidth = false; hlg.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(1); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(2); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(3); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(4); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(5); }); AddSaveButton(row.transform, imgFilePath + "saveButton.png", () => { SaveSlot(6); }); ////////////////////////// var row2 = CreateSpacer(true); row2.height =80f; var hlg2 = row2.gameObject.AddComponent(); hlg2.spacing = 10f; hlg2.childAlignment = TextAnchor.UpperCenter; hlg2.childForceExpandWidth = false; hlg2.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row2.transform, imgFilePath + "restoreButton1.png", () => { RestoreSlot(1); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton2.png" , () => { RestoreSlot(2); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton3.png", () => { RestoreSlot(3); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton4.png", () => { RestoreSlot(4); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton5.png", () => { RestoreSlot(5); }); AddSaveButton(row2.transform, imgFilePath + "restoreButton6.png", () => { RestoreSlot(6); }); /////////////////////////// var row3 = CreateSpacer(true); row3.height =30f; var hlg3 = row3.gameObject.AddComponent(); hlg3.spacing = 10f; hlg3.childAlignment = TextAnchor.UpperCenter; hlg3.childForceExpandWidth = false; hlg3.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(1); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(2); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(3); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(4); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(5); }); AddSaveButton(row3.transform, imgFilePath + "resetButton.png", () => { RestoreSelectionOnly(6); }); ////////////////////////// ////////////////////////////////////////////////// /* var purgeBtn = CreateButton("Purge Invalid Selections", true); purgeBtn.button.onClick.AddListener(() => { PurgeInvalidSelections(); }); */ //////////////////////////////////////////////////// var clearBtn = CreateButton("Clear Selection", true); clearBtn.button.onClick.AddListener(ClearAllSelection); /////////////////////////////////////////////////////// var row5 = CreateSpacer(true); row5.height =60f; var hlg5 = row5.gameObject.AddComponent(); hlg5.spacing = 10f; hlg5.childAlignment = TextAnchor.UpperCenter; hlg5.childForceExpandWidth = false; hlg5.childForceExpandHeight = false; // ボタン呼び出しだけ残す AddSaveButton(row5.transform, imgFilePath + "centerOnly.png", () => { KeepCenterVerticesBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "centerRemove.png", () => { RemoveCenterVerticesFromSelection(); }); AddSaveButton(row5.transform, imgFilePath + "leftOnly.png", () => { KeepLeftSideSelectionBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "rightOnly.png", () => { KeepRightSideSelectionBySymMap(); }); AddSaveButton(row5.transform, imgFilePath + "mirrorX.png", () => { MirrorSelectedVertices(); }); ////////////////////////////////////////////////////////// /* var symBtn = CreateButton("Mirror X Flip", true); symBtn.button.onClick.AddListener(() => { MirrorSelectedVertices(); //SuperController.LogMessage("Mirror X Flip"); }); */ /////////////////////////////////////////////////////// //UIDynamicButton btnKeepLeft = CreateButton("One Side Only", true); //btnKeepLeft.button.onClick.AddListener(KeepLeftSideSelectionBySymMap); /* UIDynamicButton bLeft = CreateButton("Left Side Only"); bLeft.button.onClick.AddListener(() => KeepLeftSideSelectionBySymMap()); UIDynamicButton bRight = CreateButton("Right Side Only"); bRight.button.onClick.AddListener(() => KeepRightSideSelectionBySymMap()); */ ////////////////////////////////////////////////////////////////// //UIDynamicButton btnKeepCenter = CreateButton("Center Only", true); //btnKeepCenter.button.onClick.AddListener(KeepCenterVerticesBySymMap); //////////////////////////////////////////////////// //片側だけの頂点編集をもとに、左右対称モーフを出力する autoSymmetryDSF = new JSONStorableBool("Auto symmetry DSF", true); RegisterBool(autoSymmetryDSF); CreateToggle(autoSymmetryDSF, true); // 新增:拆分对称 DSF 的开关(仅在 Auto symmetry DSF 打开时才有意义) splitSymmetryDSF = new JSONStorableBool("Split symmetry DSF (Left/Right files)", false); RegisterBool(splitSymmetryDSF); // 将该 Toggle 放在 UI 中(默认 false) CreateToggle(splitSymmetryDSF, true); ////////////////////////////////////////////////// CreateButton("Generate Morph(.dsf)",true).button.onClick.AddListener(ExportMorphDSF); /////////////////////////////////////////////// //Morphフィルターワード入力フィールド var tf = CreateTextField(new JSONStorableString(name, "Filtering Word"), true); _editableTextField = tf.gameObject.AddComponent(); _editableTextField.textComponent = tf.UItext; _editableTextField.text = "Instant"; tf.UItext.fontSize = 36; tf.backgroundColor = Color.white; var layout = tf.GetComponent(); if (layout == null) layout = tf.gameObject.AddComponent(); layout.minHeight = 0f; layout.preferredHeight = 48f; // ←高さを1行化 _editableTextField.onValueChanged.AddListener(value => { SuperController.LogMessage("Search Morph by " + _editableTextField.text); FilterMorphs(value); }); ///////////////////////////////////////////////// // ▼ Morph再リスト///// var reloadBtn = CreateButton("Reload Instant Morphs", true); reloadBtn.button.onClick.AddListener(() => { FilterMorphs(_editableTextField.text); SuperController.LogMessage("Refresh Instant Morphs by" + _editableTextField.text); BuildMorphSliders(); }); ///////////////////////////////////////////////// //スライダーMax値の設定 // Init() 内:共通Max入力欄 /* var maxTF = CreateTextField(new JSONStorableString(name, "Morph Max"), true); _maxInput = maxTF.gameObject.AddComponent(); _maxInput.textComponent = maxTF.UItext; _maxInput.text = _globalMax.ToString(System.Globalization.CultureInfo.InvariantCulture); maxTF.UItext.fontSize = 28; maxTF.backgroundColor = Color.white; var maxLayout = maxTF.GetComponent(); if (maxLayout == null) maxLayout = maxTF.gameObject.AddComponent(); maxLayout.preferredHeight = 40f; _maxInput.onEndEdit.AddListener(val => { float f; if (float.TryParse(val, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) { _globalMax = f; BuildMorphSliders(); // ← 再生成 } else { SuperController.LogError("Invalid Morph Max: " + val); _maxInput.text = _globalMax.ToString(System.Globalization.CultureInfo.InvariantCulture); } }); */ /////////////////////////////////////////////////////////// //左カラム//////////////////////////////////// CreateButton("Re-Bake CurrentPose as BaseMesh", false).button.onClick.AddListener(() => { CaptureCurrentPose(); }); //////////////////////////////////////////////////////// //Gen頂点だけを拾うチェックボックス(既定OFF=Body優先) genSelectOnly = new JSONStorableBool("Genital vertices only", false); //CreateToggle(genSelectOnly, false); var genSelectMode = new JSONStorableStringChooser( "Target Mesh", new List() { "Body", "Genital" }, genSelectOnly.val ? "Genital" : "Body", "Target Mesh" ); CreatePopup(genSelectMode, false); // ★ プルダウン選択で Bool を切り替え genSelectMode.setCallbackFunction = (string val) => { genSelectOnly.val = (val == "Genital"); }; //////////////////////////////////////////////////////// hoverDisplayJSON = new JSONStorableBool("Show Hover", true); //CreateToggle(hoverDisplayJSON, false); // 青マーカ用トグル originalVerticesJSON = new JSONStorableBool("Show Original vertices (blue)", true); RegisterBool(originalVerticesJSON); //CreateToggle(originalVerticesJSON); // コメントアウト //originalVerticesJSON.setCallbackFunction(true); originalVerticesJSON.setCallbackFunction = (bool val) => { foreach (var kv in baseMarkers) UnityEngine.Object.Destroy(kv.Value); baseMarkers.Clear(); if (val) { foreach (int index in selectedVertices) { GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Quad); UnityEngine.Object.DestroyImmediate(marker.GetComponent()); marker.layer = LayerMask.NameToLayer("Ignore Raycast"); marker.transform.localScale = Vector3.one * 0.0010f;//青マーカのサイズ marker.transform.SetParent(markerRoot.transform, false); marker.transform.localPosition = originalVertices[index]; // ★必ず original を参照 var mat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); if (mat != null) { mat.SetFloat("_Offset", 1f); mat.SetFloat("_MinAlpha", 1f); mat.color = Color.blue; var rend = marker.GetComponent(); rend.material = mat; rend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; rend.receiveShadows = false; // ★ 青マーカは奥に描画されるよう固定 rend.material.renderQueue = 2999; } baseMarkers[index] = marker; } } }; /////////////////////// // screen-relative brush radius brushRadiusSlider = new JSONStorableFloat("Brush Radius (screen)", 0.03f, 0.001f, 0.2f, true, true); RegisterFloat(brushRadiusSlider); CreateSlider(brushRadiusSlider); ///////////////////// var modes = new List { "FrontOnly", "Through" }; depthModeJSON = new JSONStorableStringChooser("PaintDepth", modes, "FrontOnly", "Brush Type"); RegisterStringChooser(depthModeJSON); CreatePopup(depthModeJSON, false); //////////////////// // X offset (meters) - visual preview xOffsetSlider = new JSONStorableFloat("X Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(xOffsetSlider); CreateSlider(xOffsetSlider, false).label = "X Offset"; // Y offset slider yOffsetSlider = new JSONStorableFloat("Y Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(yOffsetSlider); CreateSlider(yOffsetSlider, false).label = "Y Offset"; // Z offset slider zOffsetSlider = new JSONStorableFloat("Z Offset", 0f, -0.2f, 0.2f, true); RegisterFloat(zOffsetSlider); CreateSlider(zOffsetSlider, false).label = "Z Offset"; // Follow brush sliders(拖拽笔刷跟随) followRangeSlider = new JSONStorableFloat("Follow Range (local)", _followRange, 0.001f, 1.0f, true, true); RegisterFloat(followRangeSlider); CreateSlider(followRangeSlider).label = "Follow Range (m)"; // 本地坐标/TPose 单位,单位为米 followWeightSlider = new JSONStorableFloat("Follow Weight", _followWeight, 0f, 1f, true, true); RegisterFloat(followWeightSlider); var followWeightUI = CreateSlider(followWeightSlider); followWeightUI.label = "Follow Weight"; // 同步到内部缓存(如果需要立即响应 UI 改变) followRangeSlider.setCallbackFunction = (float _) => { _followRange = Mathf.Max(0.000001f, followRangeSlider.val); }; followWeightSlider.setCallbackFunction = (float _) => { _followWeight = Mathf.Clamp01(followWeightSlider.val); }; // 初始化缓存(确保默认值生效) _followRange = followRangeSlider != null ? followRangeSlider.val : _followRange; _followWeight = followWeightSlider != null ? followWeightSlider.val : _followWeight; _lastSliderOffset = new Vector3( xOffsetSlider != null ? xOffsetSlider.val : 0f, yOffsetSlider != null ? yOffsetSlider.val : 0f, zOffsetSlider != null ? zOffsetSlider.val : 0f ); // X scale: pivot is x=0 xScaleSlider = new JSONStorableFloat("X Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(xScaleSlider); CreateSlider(xScaleSlider, false); // Y scale yScaleSlider = new JSONStorableFloat("Y Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(yScaleSlider); CreateSlider(yScaleSlider, false); // Z scale zScaleSlider = new JSONStorableFloat("Z Scale (unsaved)", 1f, 0f, 2f, true); RegisterFloat(zScaleSlider); CreateSlider(zScaleSlider, false); ////////////////////// // === 回転スライダー === xRotateSlider = new JSONStorableFloat("X Rotate", 0f, -180f, 180f, true); RegisterFloat(xRotateSlider); CreateSlider(xRotateSlider, false); yRotateSlider = new JSONStorableFloat("Y Rotate", 0f, -180f, 180f, true); RegisterFloat(yRotateSlider); CreateSlider(yRotateSlider, false); zRotateSlider = new JSONStorableFloat("Z Rotate", 0f, -180f, 180f, true); RegisterFloat(zRotateSlider); CreateSlider(zRotateSlider, false); // 既存の setCallbackFunction 行を置き換え xOffsetSlider.setCallbackFunction = (float _) => { _xOffset = xOffsetSlider.val; ApplyXYOffsetsToSelected(); }; yOffsetSlider.setCallbackFunction = (float _) => { _yOffset = yOffsetSlider.val; ApplyXYOffsetsToSelected(); }; zOffsetSlider.setCallbackFunction = (float _) => { _zOffset = zOffsetSlider.val; ApplyXYOffsetsToSelected(); }; xScaleSlider.setCallbackFunction = (float _) => { _sxVal = xScaleSlider.val; ApplyXYOffsetsToSelected(); }; yScaleSlider.setCallbackFunction = (float _) => { _syVal = yScaleSlider.val; ApplyXYOffsetsToSelected(); }; zScaleSlider.setCallbackFunction = (float _) => { _szVal = zScaleSlider.val; ApplyXYOffsetsToSelected(); }; xRotateSlider.setCallbackFunction = (float _) => { _xRot = xRotateSlider.val; ApplyXYOffsetsToSelected(); }; yRotateSlider.setCallbackFunction = (float _) => { _yRot = yRotateSlider.val; ApplyXYOffsetsToSelected(); }; zRotateSlider.setCallbackFunction = (float _) => { _zRot = zRotateSlider.val; ApplyXYOffsetsToSelected(); }; // Export scale exportScaleSlider = new JSONStorableFloat("Export Delta Scale", 100f, 1f, 200f, true, true); //RegisterFloat(exportScaleSlider); //CreateSlider(exportScaleSlider).label = "Export Delta Scale (x)"; /////////////////////////////////////// brushRadiusSlider.setCallbackFunction = (float val) => { if (paintCircle != null && hasLastHit) { float worldRadius = ScreenSpaceToWorldRadius(lastHitPoint, val * 1000f); UpdatePaintCircle(lastHitPoint, lastHitNormal, worldRadius); } }; try { skin = containingAtom.GetComponentInChildren(); if (skin == null) { SuperController.LogError("No DAZMergedSkinV2 found."); return; } var rawVerts = skin.rawVertsBuffer; meshVertices = new Vector3[rawVerts.count]; rawVerts.GetData(meshVertices, 0, 0, rawVerts.count); // ★ 不変コピーを初期化 originalVertices = (Vector3[])meshVertices.Clone(); Mesh mesh = new Mesh(); mesh.vertices = meshVertices; mesh.triangles = skin.dazMesh.baseTriangles; meshTriangles = mesh.triangles; meshColliderContainer = CreateChildObject(containingAtom.gameObject, "VertexMultiLogger_MeshCollider"); meshColliderContainer.layer = LayerMask.NameToLayer("Ignore Raycast"); meshCollider = meshColliderContainer.AddComponent(); meshColliderContainer.layer = LayerMask.NameToLayer("Ignore Raycast"); meshCollider.sharedMesh = mesh; meshCollider.isTrigger = true; meshCollider.convex = false; meshCollider.enabled = true; markerRoot = CreateChildObject(containingAtom.gameObject, "VertexMarkers"); markerRoot.layer = LayerMask.NameToLayer("Ignore Raycast"); hoverMarker = CreateMarker(Color.yellow, 0.0012f); hoverMarker.name = "HoverMarker"; hoverMarker.transform.SetParent(markerRoot.transform, false); hoverMarker.SetActive(false); paintCircle = CreateRingMarker(Color.red, 0.01f, 0.0006f, 64); paintCircle.name = "PaintCircle"; paintCircle.transform.SetParent(markerRoot.transform, false); paintCircle.SetActive(false); //SuperController.LogMessage("VertexMultiSelectLogger ready."); // ポーズ前の頂点をクローン baseVertices = (Vector3[])skin.dazMesh.baseVertices.Clone(); baseNormals = (Vector3[])skin.dazMesh.baseNormals.Clone(); meshNormals = new Vector3[skin.normalsBuffer.count]; } catch (Exception e) { SuperController.LogError("Init error: " + e); } } //INITここまで/////////////////////////////////////////////////// ///////////////////////////////////////////////////// private void Update() { Camera cam = GetActiveCamera(); if (cam == null) { ResetHover(); return; } bool ctrl = Input.GetKey(KeyCode.LeftControl); bool alt = Input.GetKey(KeyCode.LeftAlt); bool __altReleased = (__prevAlt && !alt); bool __ctrlReleased = (__prevCtrl && !ctrl); __prevAlt = alt; __prevCtrl = ctrl; // ==== Ctrl/Alt 離された瞬間の仮処理 ==== if (__altReleased){ //SuperController.LogMessage("[KEYUP] Alt released"); paintCircle.SetActive(false); BuildSelectionSurface(); } if (__ctrlReleased){ //SuperController.LogMessage("[KEYUP] Ctrl released"); paintCircle.SetActive(false); BuildSelectionSurface(); } // ==== 入力がまったく無く、ボタンも押されておらず、離された瞬間でもないならスキップ ==== if ( !Input.GetMouseButton(0) && !Input.GetMouseButton(1) && !Input.GetMouseButtonUp(0) && !Input.GetMouseButtonUp(1) && !Input.GetMouseButtonDown(0) && !Input.GetMouseButtonDown(1) && !Input.anyKey ) return; // ==== 青クリックの確定(RESET) ==== if (!dragging && Input.GetMouseButtonUp(0) && pendingReset >= 0) { meshVertices[pendingReset] = originalVertices[pendingReset]; GameObject green = null; if (vertexMarkers.TryGetValue(pendingReset, out green) && green != null) green.transform.localPosition = originalVertices[pendingReset]; pendingReset = -1; BuildSelectionSurface(); } // ===== ドラッグ処理 ===== if (dragging && dragIndex >= 0) { HandleDragging(cam); } // ===== MouseUp 検出 ===== if (dragging && Input.GetMouseButtonUp(0)) { //SuperController.LogMessage($"[DRAG-END] vertex={dragIndex}"); BuildSelectionSurface(); // ★ 追加: 黄色ライン非表示 if (dragLine != null) { dragLine.enabled = false; } dragging = false; meshVertices[dragIndex] = vertexMarkers[dragIndex].transform.localPosition; Vector3 orig = originalVertices[dragIndex]; Vector3 cur = meshVertices[dragIndex]; float side = (Mathf.Abs(orig.x) < SYM_EPS) ? 0f : Mathf.Sign(orig.x); //Vector3 offset = new Vector3(side * _xOffset, _yOffset, _zOffset); Vector3 offset = new Vector3(_xOffset, _yOffset, _zOffset); float pivotY = 0f, pivotZ = 0f; int n = 0; foreach (int i in selectedVertices) { if (i < 0 || i >= originalVertices.Length) continue; Vector3 o = originalVertices[i]; pivotY += o.y; pivotZ += o.z; n++; } if (n > 0) { pivotY /= n; pivotZ /= n; } Vector3 scaled = new Vector3( orig.x * _sxVal, pivotY + (orig.y - pivotY) * _syVal, pivotZ + (orig.z - pivotZ) * _szVal ); dragDelta[dragIndex] = cur - (scaled + offset); dragIndex = -1; if (dragLine != null) dragLine.enabled = false; SetAllFreeControllersCanGrab(true); } ///////////////////////////////////////////////////////////////////// // ===== Ctrl/Alt 押下中は常にペイント円 ===== HandlePaint(cam, ctrl, alt); goto HOVER_ONLY; HOVER_ONLY: if (hoverDisplayJSON != null && !hoverDisplayJSON.val) return; int candidate = -1; float bestPx2 = float.MaxValue; Vector3 bestLocal = Vector3.zero; float thresholdPx = 10f; float thr2 = thresholdPx * thresholdPx; if (bestPx2 > thr2) candidate = -1; // ★ 緑マーカ(選択済み)探索 if (selectedVertices != null && selectedVertices.Count > 0) { Vector3 mouse = Input.mousePosition; foreach (int vid in selectedVertices) { Vector3 local = meshVertices[vid]; GameObject mkTmp = null; if (vertexMarkers != null && vertexMarkers.TryGetValue(vid, out mkTmp) && mkTmp != null) local = mkTmp.transform.localPosition; Vector3 world = markerRoot.transform.TransformPoint(local); Vector3 sp = cam.WorldToScreenPoint(world); Vector2 dv = new Vector2(sp.x - mouse.x, sp.y - mouse.y); float d2 = dv.x * dv.x + dv.y * dv.y; if (d2 < bestPx2 && d2 <= thr2 && sp.z > 0f) { bestPx2 = d2; candidate = vid; bestLocal = local; } } } // ★ 青マーカ(original)探索 if (originalVerticesJSON != null && originalVerticesJSON.val && baseMarkers.Count > 0) { Vector3 mouse = Input.mousePosition; foreach (var kv in baseMarkers) { int vid = kv.Key; GameObject mkB = kv.Value; if (mkB == null) continue; Vector3 world = markerRoot.transform.TransformPoint(mkB.transform.localPosition); Vector3 sp = cam.WorldToScreenPoint(world); Vector2 dv = new Vector2(sp.x - mouse.x, sp.y - mouse.y); float d2 = dv.x * dv.x + dv.y * dv.y; if (d2 < bestPx2 && d2 <= thr2 && sp.z > 0f) { bestPx2 = d2; candidate = vid; bestLocal = mkB.transform.localPosition; } } } // ===== hoverMarker 更新(削除済み) ===== if (candidate >= 0) { if (candidate != lastHovered) { skin.normalsBuffer.GetData(meshNormals, 0, 0, meshNormals.Length); Vector3 nrmBase = baseNormals[candidate]; Vector3 nrmCur = meshNormals[candidate]; lastHovered = candidate; } bool _ctrl = Input.GetKey(KeyCode.LeftControl); bool _alt = Input.GetKey(KeyCode.LeftAlt); if (_alt ^ _ctrl) { if (candidate != _lastPaintVertex || _alt != _lastAlt || _ctrl != _lastCtrl) { if (_alt) { if (selectedVertices.Remove(candidate)) RemoveMarkerAt(candidate); GameObject b; if (baseMarkers.TryGetValue(candidate, out b) && b != null) { UnityEngine.Object.Destroy(b); baseMarkers.Remove(candidate); } } else { if (!selectedVertices.Contains(candidate)) { selectedVertices.Add(candidate); CreateSelectionMarker(candidate); } } BuildSelectionSurface(); _lastPaintVertex = candidate; _lastAlt = _alt; _lastCtrl = _ctrl; } } if (alt) { if (selectedVertices.Remove(candidate)) RemoveMarkerAt(candidate); GameObject b; if (baseMarkers.TryGetValue(candidate, out b) && b != null) { UnityEngine.Object.Destroy(b); baseMarkers.Remove(candidate); } } else if (ctrl) { if (!selectedVertices.Contains(candidate)) { selectedVertices.Add(candidate); CreateSelectionMarker(candidate); } } // ===== 左クリック処理 ===== if (!ctrl && !alt && Input.GetMouseButtonDown(0)) { GameObject mkGreen = null; bool hitGreen = false; if (vertexMarkers.TryGetValue(candidate, out mkGreen) && mkGreen != null) { if (mkGreen.transform.localPosition == bestLocal) { Renderer rg = mkGreen.GetComponent(); if (rg != null && rg.material != null && rg.material.color == Color.green) { hitGreen = true; rg.material.renderQueue = 3001; } } } GameObject mkBlue = null; bool hitBlue = false; if (baseMarkers.TryGetValue(candidate, out mkBlue) && mkBlue != null) { if (mkBlue.transform.localPosition == bestLocal) { Renderer rb = mkBlue.GetComponent(); if (rb != null && rb.material != null && rb.material.color == Color.blue) hitBlue = true; } } if (hitGreen) { dragIndex = candidate; // --- 初始化 sculptBaseVertices(记录当前 mesh 状态作为拖拽基准) --- if (meshVertices != null) { if (sculptBaseVertices == null || sculptBaseVertices.Length != meshVertices.Length) sculptBaseVertices = new Vector3[meshVertices.Length]; System.Array.Copy(meshVertices, sculptBaseVertices, meshVertices.Length); } dragging = true; SetAllFreeControllersCanGrab(false); Vector3 startWorld = markerRoot.transform.TransformPoint(mkGreen.transform.localPosition); dragStartDepth = cam.WorldToScreenPoint(startWorld).z; return; } else if (hitBlue) { dragging = false; dragIndex = -1; pendingReset = candidate; return; } } } // ===== 通常スキンhover ===== if (meshCollider == null) { ResetHover(); return; } { Ray ray = cam.ScreenPointToRay(Input.mousePosition); int lm = 1 << meshCollider.gameObject.layer; float maxDist = 100f; RaycastHit hit; bool got = meshCollider.Raycast(ray, out hit, maxDist); if (!got) { float sphere = Mathf.Max(0.0005f, (brushRadiusSlider != null ? brushRadiusSlider.val * 0.25f : 0.005f)); got = Physics.SphereCast(ray, sphere, out hit, maxDist, lm, QueryTriggerInteraction.Ignore); if (!got) { ResetHover(); return; } } int triIndex = hit.triangleIndex; if (triIndex >= 0) { int v0 = meshTriangles[triIndex * 3 + 0]; int v1 = meshTriangles[triIndex * 3 + 1]; int v2 = meshTriangles[triIndex * 3 + 2]; Vector3 p = hit.point; float d0 = (p - meshVertices[v0]).sqrMagnitude; float d1 = (p - meshVertices[v1]).sqrMagnitude; float d2 = (p - meshVertices[v2]).sqrMagnitude; int nearest = v0; float best = d0; if (d1 < best) { nearest = v1; best = d1; } if (d2 < best) { nearest = v2; } if (nearest != lastHovered) { lastHovered = nearest; } } } } //Updateここまで////////////////////////////////////////////////////// private Camera GetActiveCamera() { Camera cam = Camera.main; if (cam != null) return cam; var cams = Camera.allCameras; for (int i = 0; i < cams.Length; i++) { if (cams[i] != null && cams[i].enabled) return cams[i]; } return GameObject.FindObjectOfType(); } private float ScreenSpaceToWorldRadius(Vector3 centerWorld, float screenRadiusPixels) { Camera cam = GetActiveCamera(); if (cam == null) return 0.01f; // フォールバック Vector3 sc = cam.WorldToScreenPoint(centerWorld); // center のスクリーン座標(zは深度) Vector3 se = new Vector3(sc.x + screenRadiusPixels, sc.y, sc.z); // 同じ z を維持して横にずらす Vector3 we = cam.ScreenToWorldPoint(se); // その位置をワールドへ return Vector3.Distance(centerWorld, we); } // --- Gen/Bodyモードに従って頂点を拾う安全版 --- // --- 統合ペイント版:Body+Gen 全域を対象 --- private const int MAX_SELECTED_VERTICES = 1000; private void SelectVerticesInCircle(Vector3 center, float radius, bool add) { if (meshVertices == null || originalVertices == null) return; float r2 = radius * radius; int maxLen = Mathf.Min(meshVertices.Length, originalVertices.Length); // Body/Gen 領域範囲 const int BODY_START = 0; const int BODY_END = 21556; const int GEN_START = 21556; const int GEN_END = 22970; int start = genSelectOnly.val ? GEN_START : BODY_START; int end = genSelectOnly.val ? GEN_END : BODY_END; if (add) { if (selectedVertices.Count >= MAX_SELECTED_VERTICES) { SuperController.LogMessage($"Vertex limit reached({MAX_SELECTED_VERTICES}Vertices)"); return; } for (int i = start; i < end; i++) { if (selectedVertices.Count >= MAX_SELECTED_VERTICES) break; Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(i, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[i]; if (!IsFinite3(cur)) continue; Vector3 d = cur - center; if (d.sqrMagnitude > r2) continue; if (!selectedVertices.Contains(i)) { selectedVertices.Add(i); CreateSelectionMarker(i); } } } else { if (selectedVertices != null && selectedVertices.Count > 0) { List snap = new List(selectedVertices); for (int s = 0; s < snap.Count; s++) { int i = snap[s]; if (i < start || i >= end) { selectedVertices.Remove(i); RemoveMarkerAt(i); continue; } Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(i, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[i]; bool invalidPos = !IsFinite3(cur); bool hit = invalidPos ? true : ((cur - center).sqrMagnitude <= r2); if (hit) { selectedVertices.Remove(i); RemoveMarkerAt(i); } } } } // 上限チェック if (selectedVertices.Count > MAX_SELECTED_VERTICES) { int removeCount = selectedVertices.Count - MAX_SELECTED_VERTICES; int removed = 0; foreach (int vid in selectedVertices.ToList()) { if (removed >= removeCount) break; selectedVertices.Remove(vid); RemoveMarkerAt(vid); removed++; } SuperController.LogMessage($"Vertex limit reached({MAX_SELECTED_VERTICES}Vertices)"); } PurgeInvalidSelections(); } ////////////////////////////////////////////////////////////// ////////////////////////////////////////////////// private void UpdatePaintCircle(Vector3 center, Vector3 normal, float radius) { if (paintCircle == null) return; var lr = paintCircle.GetComponent(); if (lr == null) return; int segments = lr.positionCount; if (segments < 3) segments = 3; Vector3[] points = new Vector3[segments]; Vector3 n = normal.normalized; //Vector3 n = Camera.main != null ? Camera.main.transform.forward : Vector3.forward; Vector3 tangent = Vector3.Cross(n, Vector3.up); if (tangent.sqrMagnitude < 1e-6f) tangent = Vector3.Cross(n, Vector3.right); tangent.Normalize(); Vector3 bitangent = Vector3.Cross(n, tangent); for (int i = 0; i < segments; i++) { float angle = 2 * Mathf.PI * i / segments; Vector3 dir = Mathf.Cos(angle) * tangent + Mathf.Sin(angle) * bitangent; points[i] = center + dir * radius; } lr.SetPositions(points); } private GameObject CreateSelectionMarker(int index) { GameObject marker = CreateMarker(Color.green, 0.0003f); marker.transform.SetParent(markerRoot.transform, false); float xOff = xOffsetSlider != null ? xOffsetSlider.val : 0f; marker.transform.localPosition = meshVertices[index] + new Vector3(xOff, 0f, 0f); vertexMarkers[index] = marker; return marker; } private GameObject CreateMarker(Color color, float scale) { GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Quad); var __col = marker.GetComponent(); if (__col) UnityEngine.Object.DestroyImmediate(__col); marker.transform.localScale = Vector3.one * scale; var mat = new Material(Shader.Find("Battlehub/RTGizmos/Handles")); if (mat != null) { mat.SetFloat("_Offset", 1f); mat.SetFloat("_MinAlpha", 1f); mat.color = color; var rend = marker.GetComponent(); rend.material = mat; rend.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; rend.receiveShadows = false; } return marker; marker.layer = LayerMask.NameToLayer("Ignore Raycast"); } private GameObject CreateRingMarker(Color color, float radius, float lineWidth = 0.0005f, int segments = 64) { GameObject go = new GameObject("RingMarker"); var lr = go.AddComponent(); go.layer = LayerMask.NameToLayer("Ignore Raycast"); lr.useWorldSpace = true; lr.loop = true; lr.positionCount = Mathf.Max(segments, 3); lr.startWidth = lineWidth; lr.endWidth = lineWidth; lr.numCornerVertices = 4; lr.numCapVertices = 2; lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; lr.receiveShadows = false; lr.alignment = LineAlignment.View; var mat = new Material(Shader.Find("Sprites/Default")); mat.color = color; lr.material = mat; UpdatePaintCircle(Vector3.zero, Vector3.forward, radius); return go; } private void RemoveMarkerAt(int index) { if (vertexMarkers.ContainsKey(index)) { UnityEngine.Object.Destroy(vertexMarkers[index]); vertexMarkers.Remove(index); } } private void ResetHover() { hoverMarker.SetActive(false); //paintCircle.SetActive(false);///////////////////////////////////////これのせいで外に出れない lastHovered = -1; } ////////////////////////////////////////////// private void OnDisable() { try { // ==== 保険:残存DragLineを削除 ==== foreach (var go in UnityEngine.Object.FindObjectsOfType()) { if (go != null && (go.name == "DragLine" || go.name.Contains("Marker"))) UnityEngine.Object.Destroy(go); } StopAllCoroutines(); // ==== 緑マーカ削除 ==== if (vertexMarkers != null) { foreach (var kv in vertexMarkers) { if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); } vertexMarkers.Clear(); } // ==== 青マーカ削除 ==== if (baseMarkers != null) { foreach (var kv in baseMarkers) { if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); } baseMarkers.Clear(); } // ==== 黄色ライン ==== if (dragLine != null) { UnityEngine.Object.Destroy(dragLine.gameObject); dragLine = null; } if (meshColliderContainer != null) UnityEngine.Object.Destroy(meshColliderContainer); if (markerRoot != null) UnityEngine.Object.Destroy(markerRoot); selectedVertices?.Clear(); meshVertices = null; originalVertices = null; meshNormals = null; } catch { } } //////////////////////////////////////////// private GameObject CreateChildObject(GameObject parent, string name) { GameObject go = new GameObject(name); go.transform.SetParent(parent.transform, false); go.layer = LayerMask.NameToLayer("Ignore Raycast"); return go; } // ===== DSF Export ======================================================== //========================================================================= private void ExportMorphDSF() { if (selectedVertices == null || selectedVertices.Count == 0) { SuperController.LogError("ExportMorphDSF: No selected vertices."); return; } const float EPS = 1e-14f; float scale = exportScaleSlider != null ? exportScaleSlider.val : 1f; // Body/Gen境界 const int BODY_MAX = 21556; // Body: 0–21555 const int GEN_COUNT = 1414; // Gen: 21556–22969 // 現在のプルダウン(Body/Gen選択) bool genOnly = (genSelectOnly != null && genSelectOnly.val); // 性別判定(0:female / 1:male) int gender = GetGenderInt(containingAtom); string gtxt = (gender == 1) ? "MALE" : (gender == 0 ? "FEMALE" : "UNKNOWN"); SuperController.LogMessage($"[Export] gender={gtxt}, target={(genOnly ? "Gen" : "Body")}"); // 出力対象のみ処理 ExportMorphDSF_Internal(genOnly, scale, EPS, BODY_MAX, GEN_COUNT); } //============================================================== // 内部処理本体(genOnly = true なら genitalia用) //============================================================== private void ExportMorphDSF_Internal(bool genOnly, float scale, float EPS, int bodyMax, int genCount) { try { if (skin != null && skin.normalsBuffer != null && meshNormals != null) skin.normalsBuffer.GetData(meshNormals, 0, 0, meshNormals.Length); BuildSymMaps(); Dictionary symMap = genOnly ? symMapGen : symMapBody; if (symMap == null) { SuperController.LogError("ExportMorphDSF_Internal: symMap is null"); return; } if (originalVertices == null || meshVertices == null) { SuperController.LogError("ExportMorphDSF_Internal: vertex arrays not initialized"); return; } int gender = GetGenderInt(containingAtom); // 0=female, 1=male bool isMale = (gender == 1); int vertexCount = genOnly ? 1414 : 21556; System.IFormatProvider ic = System.Globalization.CultureInfo.InvariantCulture; int maxLen = Mathf.Min(originalVertices.Length, meshVertices.Length); Quaternion rotTP = ComputeTPoseRotation(new List(selectedVertices)); Quaternion invRotTP = Quaternion.Inverse(rotTP); bool doSplit = (autoSymmetryDSF != null && autoSymmetryDSF.val) && (splitSymmetryDSF != null && splitSymmetryDSF.val); // ---- 保留原有单文件逻辑(当不做拆分时) ---- if (!doSplit) { List deltaLines = new List(); HashSet usedIds = new HashSet(); foreach (int idx in selectedVertices) { if (idx < 0 || idx >= maxLen) continue; if (genOnly && idx < bodyMax) continue; if (!genOnly && idx >= bodyMax) continue; Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[idx]; Vector3 orig = originalVertices[idx]; if (!IsFinite3(cur) || !IsFinite3(orig)) continue; Vector3 dispCur = cur - orig; if (!IsFinite3(dispCur) || dispCur.sqrMagnitude < EPS) continue; Vector3 dTP = invRotTP * dispCur; Vector3 d = new Vector3(-dTP.x, dTP.y, dTP.z) * 100f; // 中央顶点 x 归零(若开启自动对称) if (autoSymmetryDSF != null && autoSymmetryDSF.val) { bool isCenter = false; if (symMap != null && symMap.ContainsKey(idx)) { if (symMap[idx] == idx) isCenter = true; } else { Vector3 baseV = originalVertices[idx]; if (Mathf.Abs(baseV.x) < 0.0001f) isCenter = true; } if (isCenter) d.x = 0f; } int outIdx = genOnly ? (idx - bodyMax) : idx; if (!usedIds.Contains(outIdx)) { usedIds.Add(outIdx); deltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } // 镜像点 if (autoSymmetryDSF != null && autoSymmetryDSF.val && symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx)) { if (pairIdx == idx) continue; int outPair = genOnly ? (pairIdx - bodyMax) : pairIdx; if (usedIds.Contains(outPair)) continue; usedIds.Add(outPair); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); deltaLines.Add(" [ " + outPair + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } int count = deltaLines.Count; if (count == 0) { SuperController.LogError("No vertices have changed. count=0 (" + (genOnly ? "Gen" : "Body") + ")"); return; } string tag = genOnly ? "(Gen)" : "(Body)"; string genderTag = isMale ? "male" : "female"; string morphName = "Instant_" + System.DateTime.Now.ToString("yyyyMMdd_HHmm_ss") + "(" + genderTag + "_" + tag.Trim('(', ')') + ")"; string hex = "0x" + UnityEngine.Random.Range(0, 0x7FFFFFFF).ToString("x"); string parent = isMale ? (genOnly ? "/data/DAZ%203D/Genesis%202/Male/Anatomy/Genesis2MaleGenitalia.dsf#GenesisMaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Male/Genesis2Male.dsf#GenesisMale-1") : (genOnly ? "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Female/Genesis2Female.dsf#GenesisFemale-1"); var sb = new System.Text.StringBuilder(4096); sb.AppendLine("{"); sb.AppendLine(" \"file_version\" : \"0.6.1.0\","); sb.AppendLine(" \"asset_info\" : {"); sb.AppendLine(" \"id\" : \"/data/DAZ%203D/Genesis%202/" + genderTag + "/Morphs/User/Product/" + morphName + ".dsf\","); sb.AppendLine(" \"type\" : \"modifier\","); sb.AppendLine(" \"contributor\" : { \"author\" : \"User\" },"); sb.AppendLine(" \"revision\" : \"0.0\","); sb.AppendLine(" \"modified\" : \"" + System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\""); sb.AppendLine(" },"); sb.AppendLine(" \"modifier_library\" : ["); sb.AppendLine(" {"); sb.AppendLine(" \"id\" : \"" + morphName + "-" + hex + "\","); sb.AppendLine(" \"name\" : \"" + morphName + "\","); sb.AppendLine(" \"parent\" : \"" + parent + "\","); sb.AppendLine(" \"presentation\" : { \"type\" : \"Modifier/Shape\", \"label\" : \"" + morphName + "\" },"); sb.AppendLine(" \"channel\" : { \"id\":\"value\",\"type\":\"float\",\"name\":\"Value\",\"label\":\"" + morphName + "\",\"auto_follow\":true,\"value\":0,\"min\":-1,\"max\":1,\"clamped\":true,\"display_as_percent\":true,\"step_size\":0.01 },"); sb.AppendLine(" \"group\" : \"/Morphs/Morph Loader\","); sb.AppendLine(" \"morph\" : {"); sb.AppendLine(" \"vertex_count\" : " + vertexCount + ","); sb.AppendLine(" \"deltas\" : {"); sb.AppendLine(" \"count\" : " + count + ","); sb.AppendLine(" \"values\" : ["); for (int i = 0; i < deltaLines.Count; i++) { sb.Append(deltaLines[i]); if (i < deltaLines.Count - 1) sb.AppendLine(","); else sb.AppendLine(); } sb.AppendLine(" ]"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" ],"); sb.AppendLine(" \"scene\" : { \"modifiers\" : [ { \"id\" : \"" + morphName + "\", \"url\" : \"#" + morphName + "-" + hex + "\" } ] }"); sb.AppendLine("}"); string dir = isMale ? (genOnly ? "Custom/Atom/Person/Morphs/male_genitalia" : "Custom/Atom/Person/Morphs/male") : (genOnly ? "Custom/Atom/Person/Morphs/female_genitalia" : "Custom/Atom/Person/Morphs/female"); if (!FileManagerSecure.DirectoryExists(dir)) FileManagerSecure.CreateDirectory(dir); string path = dir + "/" + morphName + ".dsf"; FileManagerSecure.WriteAllText(path, sb.ToString()); SuperController.LogMessage((genOnly ? "[Gen]" : "[Body]") + " DSF exported: " + path + " (gender=" + genderTag + ", count=" + count + ")"); return; } // ---- 拆分为左右两个 DSF 的逻辑 ---- else { List leftDeltaLines = new List(); List rightDeltaLines = new List(); HashSet usedLeft = new HashSet(); HashSet usedRight = new HashSet(); const float CENTER_EPS = 0.0001f; foreach (int idx in selectedVertices) { if (idx < 0 || idx >= maxLen) continue; if (genOnly && idx < bodyMax) continue; if (!genOnly && idx >= bodyMax) continue; Vector3 cur; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) cur = go.transform.localPosition; else cur = meshVertices[idx]; Vector3 orig = originalVertices[idx]; if (!IsFinite3(cur) || !IsFinite3(orig)) continue; Vector3 dispCur = cur - orig; if (!IsFinite3(dispCur) || dispCur.sqrMagnitude < EPS) continue; Vector3 dTP = invRotTP * dispCur; Vector3 d = new Vector3(-dTP.x, dTP.y, dTP.z) * 100f; bool isCenter = Mathf.Abs(orig.x) < CENTER_EPS; if (isCenter) { d.x = 0f; int outIdx = genOnly ? (idx - bodyMax) : idx; if (!usedLeft.Contains(outIdx)) { usedLeft.Add(outIdx); leftDeltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (!usedRight.Contains(outIdx)) { usedRight.Add(outIdx); rightDeltaLines.Add(" [ " + outIdx + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } continue; } if (orig.x < 0f) { int outIdxLeft = genOnly ? (idx - bodyMax) : idx; if (!usedLeft.Contains(outIdxLeft)) { usedLeft.Add(outIdxLeft); leftDeltaLines.Add(" [ " + outIdxLeft + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx) && pairIdx != idx) { int outIdxRight = genOnly ? (pairIdx - bodyMax) : pairIdx; if (!usedRight.Contains(outIdxRight)) { usedRight.Add(outIdxRight); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); rightDeltaLines.Add(" [ " + outIdxRight + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } } else { int outIdxRight = genOnly ? (idx - bodyMax) : idx; if (!usedRight.Contains(outIdxRight)) { usedRight.Add(outIdxRight); rightDeltaLines.Add(" [ " + outIdxRight + ", " + d.x.ToString("G9", ic) + ", " + d.y.ToString("G9", ic) + ", " + d.z.ToString("G9", ic) + " ]"); } if (symMap != null) { int pairIdx; if (symMap.TryGetValue(idx, out pairIdx) && pairIdx != idx) { int outIdxLeft = genOnly ? (pairIdx - bodyMax) : pairIdx; if (!usedLeft.Contains(outIdxLeft)) { usedLeft.Add(outIdxLeft); Vector3 dMirror = new Vector3(-d.x, d.y, d.z); leftDeltaLines.Add(" [ " + outIdxLeft + ", " + dMirror.x.ToString("G9", ic) + ", " + dMirror.y.ToString("G9", ic) + ", " + dMirror.z.ToString("G9", ic) + " ]"); } } } } } string tagBase = genOnly ? "(Gen)" : "(Body)"; string genderTag = isMale ? "male" : "female"; string hex = "0x" + UnityEngine.Random.Range(0, 0x7FFFFFFF).ToString("x"); string dir = isMale ? (genOnly ? "Custom/Atom/Person/Morphs/male_genitalia" : "Custom/Atom/Person/Morphs/male") : (genOnly ? "Custom/Atom/Person/Morphs/female_genitalia" : "Custom/Atom/Person/Morphs/female"); if (!FileManagerSecure.DirectoryExists(dir)) FileManagerSecure.CreateDirectory(dir); // helper 写文件 System.Action, string> WriteDsf = (deltaList, sideLabel) => { if (deltaList == null || deltaList.Count == 0) return; string morphNameSide = "Instant_" + System.DateTime.Now.ToString("yyyyMMdd_HHmm_ss") + "(" + genderTag + "_" + tagBase.Trim('(', ')') + "-" + sideLabel + ")"; var sbSide = new System.Text.StringBuilder(4096); sbSide.AppendLine("{"); sbSide.AppendLine(" \"file_version\" : \"0.6.1.0\","); sbSide.AppendLine(" \"asset_info\" : {"); sbSide.AppendLine(" \"id\" : \"/data/DAZ%203D/Genesis%202/" + genderTag + "/Morphs/User/Product/" + morphNameSide + ".dsf\","); sbSide.AppendLine(" \"type\" : \"modifier\","); sbSide.AppendLine(" \"contributor\" : { \"author\" : \"User\" },"); sbSide.AppendLine(" \"revision\" : \"0.0\","); sbSide.AppendLine(" \"modified\" : \"" + System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") + "\""); sbSide.AppendLine(" },"); sbSide.AppendLine(" \"modifier_library\" : ["); sbSide.AppendLine(" {"); sbSide.AppendLine(" \"id\" : \"" + morphNameSide + "-" + hex + "\","); sbSide.AppendLine(" \"name\" : \"" + morphNameSide + "\","); string parentSide = isMale ? (genOnly ? "/data/DAZ%203D/Genesis%202/Male/Anatomy/Genesis2MaleGenitalia.dsf#GenesisMaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Male/Genesis2Male.dsf#GenesisMale-1") : (genOnly ? "/data/DAZ%203D/Genesis%202/Female/Anatomy/Genesis2FemaleGenitalia.dsf#GenesisFemaleGenitalia-1" : "/data/DAZ%203D/Genesis%202/Female/Genesis2Female.dsf#GenesisFemale-1"); sbSide.AppendLine(" \"parent\" : \"" + parentSide + "\","); sbSide.AppendLine(" \"presentation\" : { \"type\" : \"Modifier/Shape\", \"label\" : \"" + morphNameSide + "\" },"); sbSide.AppendLine(" \"channel\" : { \"id\":\"value\",\"type\":\"float\",\"name\":\"Value\",\"label\":\"" + morphNameSide + "\",\"auto_follow\":true,\"value\":0,\"min\":-1,\"max\":1,\"clamped\":true,\"display_as_percent\":true,\"step_size\":0.01 },"); sbSide.AppendLine(" \"group\" : \"/Morphs/Morph Loader\","); sbSide.AppendLine(" \"morph\" : {"); sbSide.AppendLine(" \"vertex_count\" : " + vertexCount + ","); sbSide.AppendLine(" \"deltas\" : {"); sbSide.AppendLine(" \"count\" : " + deltaList.Count + ","); sbSide.AppendLine(" \"values\" : ["); for (int i = 0; i < deltaList.Count; i++) { sbSide.Append(deltaList[i]); if (i < deltaList.Count - 1) sbSide.AppendLine(","); else sbSide.AppendLine(); } sbSide.AppendLine(" ]"); sbSide.AppendLine(" }"); sbSide.AppendLine(" }"); sbSide.AppendLine(" }"); sbSide.AppendLine(" ],"); sbSide.AppendLine(" \"scene\" : { \"modifiers\" : [ { \"id\" : \"" + morphNameSide + "\", \"url\" : \"#" + morphNameSide + "-" + hex + "\" } ] }"); sbSide.AppendLine("}"); string pathSide = dir + "/" + morphNameSide + ".dsf"; FileManagerSecure.WriteAllText(pathSide, sbSide.ToString()); SuperController.LogMessage("[Split DSF] " + sideLabel + " DSF exported: " + pathSide + " (gender=" + genderTag + ", count=" + deltaList.Count + ")"); }; WriteDsf(leftDeltaLines, "Left"); WriteDsf(rightDeltaLines, "Right"); if ((leftDeltaLines == null || leftDeltaLines.Count == 0) && (rightDeltaLines == null || rightDeltaLines.Count == 0)) { SuperController.LogError("Split symmetry DSF selected but no deltas found for either side."); } return; } } catch (System.Exception ex) { SuperController.LogError("ExportMorphDSF_Internal ERROR: " + ex.Message); } } ///////////////////////////////////////////////////////////////////// private void MirrorSelectedVertices() { BuildSymMaps(); bool genOnly = (genSelectOnly != null && genSelectOnly.val); Dictionary symMapLocal = genOnly ? symMapGen : symMapBody; if (symMapLocal == null || symMapLocal.Count == 0) { SuperController.LogMessage("Symmetry map not built."); return; } if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; List snap = new List(selectedVertices); // 古いマーカ削除 foreach (int vid in snap) { GameObject go; if (vertexMarkers.TryGetValue(vid, out go) && go != null) UnityEngine.Object.Destroy(go); vertexMarkers.Remove(vid); } selectedVertices.Clear(); int mirrored = 0, centered = 0; foreach (int i in snap) { int j; bool hasPair = symMapLocal.TryGetValue(i, out j); if (!hasPair) { // === センター頂点:ペア無し === CreateSelectionMarker(i); GameObject c; if (vertexMarkers.TryGetValue(i, out c) && c != null) c.transform.localPosition = originalVertices[i] + dragDelta[i]; selectedVertices.Add(i); centered++; continue; } // --- 通常ミラー処理 --- if (i < 0 || j < 0 || i >= originalVertices.Length || j >= originalVertices.Length) continue; Vector3 delta = dragDelta[i]; Vector3 deltaMirror = new Vector3(-delta.x, delta.y, delta.z); dragDelta[j] = deltaMirror; Vector3 dstPos = originalVertices[j] + deltaMirror; CreateSelectionMarker(j); GameObject newGo; if (vertexMarkers.TryGetValue(j, out newGo) && newGo != null) newGo.transform.localPosition = dstPos; selectedVertices.Add(j); mirrored++; } SuperController.LogMessage($"Symmetry mirrored {mirrored} vertices, kept {centered} center vertices."); ApplyXYOffsetsToSelected(); originalVerticesJSON.setCallbackFunction(true); } ///////////////////////////////////////////////////////////// private int FindNearestVertex(Vector3 target, float threshold) { float best = threshold * threshold; int bestIndex = -1; for (int i = 0; i < meshVertices.Length; i++) { float d2 = (meshVertices[i] - target).sqrMagnitude; if (d2 < best) { best = d2; bestIndex = i; } } return bestIndex; } //////////////////////////////////////////////////////// /* void EnsureSymMapBuilt() { if (symMapBuilt) return; symMap.Clear(); foreach (var p in SYM_PAIRS) { int a = p.Key, b = p.Value; if (a >= 0 && a < meshVertices.Length && b >= 0 && b < meshVertices.Length) { symMap[a] = b; symMap[b] = a; } } symMapBuilt = true; } */ //////////////////////////////////////////////////////// ///////////////////////////////////////////////////// /* // 置換:タプルやローカル関数を使わない互換版 void AutoBuildSymMapFromPositions() { if (meshVertices == null) { SuperController.LogError("meshVertices==null"); return; } symMap.Clear(); // 文字列キー: "|ax|,y,z" 量子化 var buckets = new Dictionary>(); for (int i = 0; i < meshVertices.Length; i++) { var v = meshVertices[i]; string key = Quantize(Mathf.Abs(v.x)) + "," + Quantize(v.y) + "," + Quantize(v.z); List list; if (!buckets.TryGetValue(key, out list)) { list = new List(); buckets[key] = list; } list.Add(i); } int made = 0, mid = 0; foreach (var kv in buckets) { var list = kv.Value; var left = new List(); var right = new List(); for (int idx = 0; idx < list.Count; idx++) { int i = list[idx]; float x = meshVertices[i].x; if (Mathf.Abs(x) < SYM_EPS) { symMap[i] = i; mid++; } else if (x < 0f) left.Add(i); else right.Add(i); } // 左→右 最近傍 for (int aIdx = 0; aIdx < left.Count; aIdx++) { int i = left[aIdx]; int best = -1; float bestD = float.PositiveInfinity; Vector3 mi = new Vector3(-meshVertices[i].x, meshVertices[i].y, meshVertices[i].z); for (int bIdx = 0; bIdx < right.Count; bIdx++) { int j = right[bIdx]; float d = (meshVertices[j] - mi).sqrMagnitude; if (d < bestD) { bestD = d; best = j; } } if (best >= 0) { symMap[i] = best; symMap[best] = i; made++; } } // 右→左 補完 for (int bIdx = 0; bIdx < right.Count; bIdx++) { int j = right[bIdx]; if (symMap.ContainsKey(j)) continue; int best = -1; float bestD = float.PositiveInfinity; Vector3 mj = new Vector3(-meshVertices[j].x, meshVertices[j].y, meshVertices[j].z); for (int aIdx = 0; aIdx < left.Count; aIdx++) { int i = left[aIdx]; float d = (meshVertices[i] - mj).sqrMagnitude; if (d < bestD) { bestD = d; best = i; } } if (best >= 0) { symMap[j] = best; symMap[best] = j; made++; } } } symMapBuilt = true; SuperController.LogMessage("Auto SymMap built: pairs:" + made + " mid:" + mid + " totalEntries:" + symMap.Count); } */ ///////////////////////////////////// // clear everything about selection, without destroying objects // 選択解除+完全リセット(頂点オフセットも原点に戻す) private void ClearAllSelection() { // 1) 選択頂点を original に戻す(マーカー&配列) if (selectedVertices != null && selectedVertices.Count > 0) { if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; foreach (int idx in selectedVertices) { meshVertices[idx] = originalVertices[idx]; dragDelta[idx] = Vector3.zero; GameObject go; if (vertexMarkers != null && vertexMarkers.TryGetValue(idx, out go) && go != null) go.transform.localPosition = originalVertices[idx]; GameObject bo; if (baseMarkers != null && baseMarkers.TryGetValue(idx, out bo) && bo != null) bo.transform.localPosition = originalVertices[idx]; } } // 2) スライダーと内部オフセット/スケール/回転を初期化(静かに) _xOffset = _yOffset = _zOffset = 0f; _sxVal = _syVal = _szVal = 1f; _xRot = _yRot = _zRot = 0f; if (xOffsetSlider != null) xOffsetSlider.valNoCallback = 0f; if (yOffsetSlider != null) yOffsetSlider.valNoCallback = 0f; if (zOffsetSlider != null) zOffsetSlider.valNoCallback = 0f; if (xScaleSlider != null) xScaleSlider.valNoCallback = 1f; if (yScaleSlider != null) yScaleSlider.valNoCallback = 1f; if (zScaleSlider != null) zScaleSlider.valNoCallback = 1f; if (xRotateSlider != null) xRotateSlider.valNoCallback = 0f; if (yRotateSlider != null) yRotateSlider.valNoCallback = 0f; if (zRotateSlider != null) zRotateSlider.valNoCallback = 0f; // 3) 選択ビジュアルのみ破棄(スロットや頂点データは保持) if (selectionSurface != null) { UnityEngine.Object.Destroy(selectionSurface); selectionSurface = null; } if (edgeLines != null) { foreach (var lr in edgeLines) if (lr != null) UnityEngine.Object.Destroy(lr.gameObject); edgeLines.Clear(); } if (vertexMarkers != null) { foreach (var kv in vertexMarkers) if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); vertexMarkers.Clear(); } if (baseMarkers != null) { foreach (var kv in baseMarkers) if (kv.Value != null) UnityEngine.Object.Destroy(kv.Value); baseMarkers.Clear(); } // 4) 選択集合だけクリア selectedVertices?.Clear(); // 5) マーカ親を再生成(残骸防止) if (markerRoot != null) UnityEngine.Object.Destroy(markerRoot); markerRoot = CreateChildObject(containingAtom.gameObject, "VertexMarkers"); markerRoot.layer = LayerMask.NameToLayer("Ignore Raycast"); // hover/painter 再生成 hoverMarker = CreateMarker(Color.yellow, 0.0005f); hoverMarker.name = "HoverMarker"; hoverMarker.transform.SetParent(markerRoot.transform, false); hoverMarker.SetActive(false); paintCircle = CreateRingMarker(Color.red, 0.01f, 0.0006f, 64); paintCircle.name = "PaintCircle"; paintCircle.transform.SetParent(markerRoot.transform, false); paintCircle.SetActive(false); // --- dragDelta配列と saveSlots は保持 --- if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; // === BuildSelectionSurfaceは呼ばない(負荷低減)=== SuperController.LogMessage("ClearAllSelection: visuals cleared, slots preserved."); } ////////////////////////////////////////////////////////////////// // 追加 private Vector3 CalcBluePos(int idx) { Vector3 orig = originalVertices[idx]; // ★左右同方向(フリップしない) return new Vector3( orig.x + _xOffset, orig.y + _yOffset, orig.z + _zOffset ); } /////////////////////////////////////////////////////////////////// // これで統一:オフセットとスケールをまとめて反映 private void ApplyXYOffsetsToSelected() { if (selectedVertices == null || selectedVertices.Count == 0) return; if (dragDelta == null || dragDelta.Length != originalVertices.Length) dragDelta = new Vector3[originalVertices.Length]; // pivot(original基準) float pivotX = 0f, pivotY = 0f, pivotZ = 0f; int n = 0; foreach (int i in selectedVertices) { if (i < 0 || i >= originalVertices.Length) continue; Vector3 o = originalVertices[i]; pivotX += o.x; pivotY += o.y; pivotZ += o.z; n++; } if (n > 0) { pivotX /= n; pivotY /= n; pivotZ /= n; } Vector3 pivot = new Vector3(pivotX, pivotY, pivotZ); // 回転クォータニオン Quaternion qx = Quaternion.AngleAxis(_xRot, Vector3.right); Quaternion qy = Quaternion.AngleAxis(_yRot, Vector3.up); Quaternion qz = Quaternion.AngleAxis(_zRot, Vector3.forward); Quaternion rot = qz * qy * qx; foreach (int idx in selectedVertices) { GameObject go; if (!vertexMarkers.TryGetValue(idx, out go) || go == null) continue; Vector3 orig = originalVertices[idx]; // pivot基準スケール float sx = pivot.x + (orig.x - pivot.x) * _sxVal; float sy = pivot.y + (orig.y - pivot.y) * _syVal; float sz = pivot.z + (orig.z - pivot.z) * _szVal; Vector3 scaled = new Vector3(sx, sy, sz); // pivot基準で回転 Vector3 p = scaled - pivot; p = rot * p; p += pivot; // オフセット+ドラッグ分 Vector3 newPos = p + new Vector3(_xOffset, _yOffset, _zOffset) + dragDelta[idx]; go.transform.localPosition = newPos; meshVertices[idx] = newPos; } BuildSelectionSurface(); } /////////////////////////////////////// void LogCenterLineVertices() { if (meshVertices == null || meshVertices.Length == 0) { SuperController.LogMessage("meshVertices が未初期化です"); return; } List centerLineVerts = new List(); for (int i = 0; i < meshVertices.Length; i++) { if (Mathf.Abs(meshVertices[i].x) < SYM_EPS) centerLineVerts.Add(i); } SuperController.LogMessage($"中心線の頂点数: {centerLineVerts.Count}"); for (int j = 0; j < centerLineVerts.Count; j += 10) // ← ここを j に変更 { int len = Math.Min(10, centerLineVerts.Count - j); List slice = centerLineVerts.GetRange(j, len); SuperController.LogMessage("Verts: " + string.Join(", ", slice.ConvertAll(k => k.ToString()).ToArray())); } } ///////////////////////////////////////// private void FilterMorphs(string keyword) { matchedMorphs.Clear(); if (string.IsNullOrEmpty(keyword) || keyword.Length < 3) { SuperController.LogMessage("Type at least 3 characters to search morphs."); return; } var person = containingAtom?.GetComponentInChildren(); if (person == null || person.morphsControlUI == null) return; int gender = GetGenderInt(containingAtom); // 0=female, 1=male foreach (var m in person.morphsControlUI.GetMorphs()) { if (m.displayName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) < 0) continue; if (gender == 0) { // female の場合 male系を除外 if (m.region != null && m.region.ToLower().Contains("male")) continue; } else if (gender == 1) { // male の場合 female系を除外 if (m.region != null && m.region.ToLower().Contains("female")) continue; } matchedMorphs.Add(m.displayName); } //if (matchedMorphs.Count == 0) // SuperController.LogMessage("No morphs matched: \"" + keyword + "\""); //else // SuperController.LogMessage("Matched morphs (" + matchedMorphs.Count + "): " + string.Join(", ", matchedMorphs)); } ///////////////////////////////////////// private void BuildMorphSliders() { // 既存スライダーを削除 foreach (var s in morphSliders) RemoveSlider(s); morphSliders.Clear(); var person = containingAtom?.GetComponentInChildren(); if (person == null || person.morphsControlUI == null) return; // 逆順で処理 for (int i = matchedMorphs.Count - 1; i >= 0; i--) { var morphName = matchedMorphs[i]; var m = person.morphsControlUI.GetMorphByDisplayName(morphName); if (m == null) continue; // --- スライダー生成 --- var jsf = new JSONStorableFloat(m.displayName, 0f, -_globalMax, _globalMax, true); jsf.setCallbackFunction = (float v) => { m.morphValue = v; }; RegisterFloat(jsf); var slider = CreateSlider(jsf, true); morphSliders.Add(slider); jsf.valNoCallback = m.morphValue; // --- body/gen 判定 --- bool isGen = false; if (!string.IsNullOrEmpty(m.region)) { isGen = m.region.IndexOf("Genital", StringComparison.OrdinalIgnoreCase) >= 0; } else { // region が空なら名前で推定 string name = m.displayName.ToLower(); isGen = name.Contains("gen"); } // --- ラベル色変更 --- var label = slider.transform.Find("Text")?.GetComponent(); if (label != null) { string lowerName = morphName.ToLower(); if (lowerName.Contains("gen")) label.color = new Color(0.8f, 0.2f, 0.4f, 1f); // gen含む → ピンク else label.color = new Color(0f, 0.3f, 0.8f, 1f); // body → 青系 } } SuperController.LogMessage("Created " + morphSliders.Count + " morph sliders."); } //////////////////////////////////////// private void SetAllFreeControllersCanGrab(bool allow) { try { if (containingAtom == null) return; var fcs = containingAtom.freeControllers; if (fcs == null) return; foreach (var fc in fcs) { if (fc == null) continue; fc.canGrabPosition = allow; } } catch (Exception) { /* 静かに失敗させる */ } } /////////////////////////////////////// GameObject selectionSurface; List edgeLines = new List(); // === OPT: Throttled wrapper === void BuildSelectionSurfaceThrottled() { if (Time.time - __lastSurfaceBuildTime < __SURFACE_BUILD_INTERVAL) return; __lastSurfaceBuildTime = Time.time; BuildSelectionSurface(); } //////////////////////////////////////// void BuildSelectionSurface() { //PruneByDomain(); PurgeInvalidSelections(); originalVerticesJSON.setCallbackFunction(true); //SuperController.LogMessage("BuildSelectionSurface & Callback"); //if (Time.frameCount % 10 != 0) return; if (meshTriangles == null || selectedVertices == null || selectedVertices.Count == 0) { if (selectionSurface != null) selectionSurface.SetActive(false); foreach (var lr in edgeLines) if (lr != null) lr.gameObject.SetActive(false); return; } // 現在の緑マーカ位置 Vector3[] currentVerts = new Vector3[meshVertices.Length]; for (int i = 0; i < meshVertices.Length; i++) { if (vertexMarkers.ContainsKey(i)) currentVerts[i] = vertexMarkers[i].transform.localPosition; else currentVerts[i] = meshVertices[i]; } // 選択三角形 List selTris = new List(); for (int i = 0; i < meshTriangles.Length; i += 3) { int a = meshTriangles[i], b = meshTriangles[i + 1], c = meshTriangles[i + 2]; if (selectedVertices.Contains(a) && selectedVertices.Contains(b) && selectedVertices.Contains(c)) { selTris.Add(a); selTris.Add(b); selTris.Add(c); } } if (selTris.Count == 0) { if (selectionSurface != null) selectionSurface.SetActive(false); foreach (var lr in edgeLines) if (lr != null) lr.gameObject.SetActive(false); return; } // ★ サーフェス生成は停止(負荷軽減) ★ /* Mesh sub = new Mesh(); sub.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; sub.vertices = currentVerts; sub.triangles = selTris.ToArray(); sub.RecalculateNormals(); SmoothPreview(sub, 2); if (selectionSurface == null) { selectionSurface = new GameObject("SelectionSurface"); selectionSurface.layer = LayerMask.NameToLayer("Ignore Raycast"); selectionSurface.transform.SetParent(markerRoot.transform, false); selectionSurface.AddComponent(); var mr = selectionSurface.AddComponent(); var mat = new Material(Shader.Find("Standard")); mat.color = new Color(0f, 1f, 0f, 0.25f); mat.SetFloat("_Mode", 3); mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); mat.renderQueue = 3000; mr.material = mat; } selectionSurface.SetActive(true); selectionSurface.GetComponent().mesh = sub; */ // 稜線再構築 foreach (var lr in edgeLines) if (lr != null) GameObject.Destroy(lr.gameObject); edgeLines.Clear(); for (int i = 0; i < selTris.Count; i += 3) { int a = selTris[i], b = selTris[i + 1], c = selTris[i + 2]; AddEdgeLine(currentVerts[a], currentVerts[b]); AddEdgeLine(currentVerts[b], currentVerts[c]); AddEdgeLine(currentVerts[c], currentVerts[a]); } } /////////////////////////////////////////////////////////////////////////////// void AddEdgeLine(Vector3 p1, Vector3 p2) { var cam = Camera.main; if (cam == null) return; // カメラ方向へ微小オフセット(0.0003m = 0.3mm) Vector3 offset = cam.transform.forward * -0.001f; p1 += offset; p2 += offset; var go = new GameObject("EdgeLine"); go.layer = LayerMask.NameToLayer("Ignore Raycast"); go.transform.SetParent(markerRoot.transform, false); var lr = go.AddComponent(); lr.positionCount = 2; lr.SetPosition(0, p1); lr.SetPosition(1, p2); lr.startWidth = lr.endWidth = 0.0003f; lr.useWorldSpace = true; var mat = new Material(Shader.Find("Sprites/Default")); mat.color = Color.black; //mat.renderQueue = 3100; // ★描画順を前へ mat.SetInt("_ZWrite", 0); // ★深度書き込みオフ lr.material = mat; edgeLines.Add(lr); } ////////////////////////////////////////////////////// // 選択メッシュに対してラプラシアンスムーズを数回かける private void SmoothPreview(Mesh mesh, int iterations = 2) { if (mesh == null) return; var verts = mesh.vertices; var tris = mesh.triangles; // 隣接リスト作成 List[] neighbors = new List[verts.Length]; for (int i = 0; i < verts.Length; i++) neighbors[i] = new List(); for (int i = 0; i < tris.Length; i += 3) { int a = tris[i], b = tris[i + 1], c = tris[i + 2]; if (!neighbors[a].Contains(b)) neighbors[a].Add(b); if (!neighbors[a].Contains(c)) neighbors[a].Add(c); if (!neighbors[b].Contains(a)) neighbors[b].Add(a); if (!neighbors[b].Contains(c)) neighbors[b].Add(c); if (!neighbors[c].Contains(a)) neighbors[c].Add(a); if (!neighbors[c].Contains(b)) neighbors[c].Add(b); } for (int it = 0; it < iterations; it++) { Vector3[] newVerts = new Vector3[verts.Length]; for (int i = 0; i < verts.Length; i++) { if (neighbors[i].Count == 0) { newVerts[i] = verts[i]; continue; } Vector3 avg = Vector3.zero; foreach (int n in neighbors[i]) avg += verts[n]; avg /= neighbors[i].Count; newVerts[i] = Vector3.Lerp(verts[i], avg, 0.5f); // 0.5で滑らかに } verts = newVerts; } mesh.vertices = verts; mesh.RecalculateNormals(); } ////////////////////////////////////////////////////////// /* private void CreateImageButton(float x, float y, string imageFilename, Action callback) { // 生の GameObject を直接生成 var go = new GameObject("MiniImageBtn", typeof(RectTransform), typeof(RawImage), typeof(Button)); go.transform.SetParent(UITransform, false); var rt = go.GetComponent(); rt.sizeDelta = new Vector2(48f, 48f); rt.anchoredPosition = new Vector2(x, y); // 自由配置が効く // 画像設定 var img = go.GetComponent(); var tex = LoadTex(imageFilename); if (tex != null) img.texture = tex; img.color = Color.white; // ボタン処理 var btn = go.GetComponent