531 lines
23 KiB
C#
531 lines
23 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
namespace XGame.Editor
|
||
{
|
||
public static class ModelTranslatorMenu
|
||
{
|
||
[MenuItem("XWorld/模型转换(减面·烘焙·XPbr)", false, 803)]
|
||
public static void Open() => ModelTranslatorWindow.Open();
|
||
}
|
||
|
||
// 减面 → 烘焙 → 转换XPbr 三级流水线,同一窗口三个折叠区,共用 Blender/Python、日志与进程管理;
|
||
// 每级成功后把产物自动填入下一级输入(减面low→烘焙low、烘焙fbx→转换fbx)。一次只跑一个进程。
|
||
public sealed class ModelTranslatorWindow : EditorWindow
|
||
{
|
||
private const string PrefPrefix = "XWorld.ModelTranslator.";
|
||
private const string DefaultBlender = @"D:\tools\Blender\blender-5.0.0-windows-x64\blender.exe";
|
||
private static readonly int[] SizeOptions = { 512, 1024, 2048, 4096 };
|
||
private static readonly string[] SizeLabels = { "512", "1024", "2048", "4096" };
|
||
private static readonly string[] ReducerLabels = { "Quad Remesher(四边)", "Collapse(塌陷)" };
|
||
private static readonly string[] UnwrapLabels = { "seam(锐边接缝)", "smart(投影)" };
|
||
|
||
// ① 减面
|
||
private string _decHigh = "";
|
||
private int _decReducer; // 0=quad 1=collapse
|
||
private int _decTris = 5000;
|
||
private bool _decAdvanced;
|
||
private int _decUnwrap; // 0=seam 1=smart
|
||
private float _decMaxOverlap = 0.08f;
|
||
private int _decQrCap = 50000;
|
||
|
||
// ② 烘焙
|
||
private string _bakeHigh = "";
|
||
private string _bakeLow = "";
|
||
private int _bakeSize = 2048;
|
||
private int _bakeSamples = 64;
|
||
private string _bakeRayDist = ""; // 空=auto 自适应
|
||
|
||
// ③ 转换 XPbr
|
||
private string _fbxPath = "";
|
||
private int _maxSize = 2048;
|
||
private string _outSubDir = "";
|
||
|
||
// 共用
|
||
private string _blenderPath = DefaultBlender;
|
||
private string _pythonPath = "python";
|
||
private bool _foldDec = true, _foldBake = true, _foldConv = true;
|
||
|
||
private Process _proc;
|
||
private Action _onSuccess; // 退出码 0 时回调(ping / 链式填充)
|
||
private string _stageName = ""; // 当前阶段名(减面/烘焙/转换),用于进度提示
|
||
private double _procStartTime; // 进程启动时刻(timeSinceStartup)
|
||
private double _lastAnimAt; // 上次动画重绘时刻(节流用)
|
||
private readonly StringBuilder _log = new StringBuilder();
|
||
private Vector2 _scroll;
|
||
// stdout/stderr 回调在后台线程触发,先入队、OnUpdate 时再合入 _log
|
||
private readonly object _lock = new object();
|
||
private readonly List<string> _pending = new List<string>();
|
||
|
||
// 仓库根 = <repo>/Client/Assets 的上两级
|
||
private static string RepoRoot =>
|
||
Path.GetFullPath(Path.Combine(Application.dataPath, "..", ".."));
|
||
|
||
public static void Open()
|
||
{
|
||
var w = GetWindow<ModelTranslatorWindow>("模型转换");
|
||
w.minSize = new Vector2(500, 560);
|
||
w.Show();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_decReducer = EditorPrefs.GetInt(PrefPrefix + "decReducer", 0);
|
||
_decTris = EditorPrefs.GetInt(PrefPrefix + "decTris", 5000);
|
||
_decUnwrap = EditorPrefs.GetInt(PrefPrefix + "decUnwrap", 0);
|
||
_decMaxOverlap = EditorPrefs.GetFloat(PrefPrefix + "decMaxOverlap", 0.08f);
|
||
_decQrCap = EditorPrefs.GetInt(PrefPrefix + "decQrCap", 50000);
|
||
_bakeSize = EditorPrefs.GetInt(PrefPrefix + "bakeSize", 2048);
|
||
_bakeSamples = EditorPrefs.GetInt(PrefPrefix + "bakeSamples", 64);
|
||
_bakeRayDist = EditorPrefs.GetString(PrefPrefix + "bakeRayDist", "");
|
||
_maxSize = EditorPrefs.GetInt(PrefPrefix + "maxSize", 2048);
|
||
_outSubDir = EditorPrefs.GetString(PrefPrefix + "outSubDir", "");
|
||
_blenderPath = EditorPrefs.GetString(PrefPrefix + "blenderPath", DefaultBlender);
|
||
_pythonPath = EditorPrefs.GetString(PrefPrefix + "pythonPath", "python");
|
||
EditorApplication.update += OnUpdate;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
EditorApplication.update -= OnUpdate;
|
||
SavePrefs();
|
||
if (_proc != null)
|
||
UnityEngine.Debug.LogWarning("[模型转换] 窗口关闭或脚本重编译中止了正在进行的处理");
|
||
KillProc();
|
||
}
|
||
|
||
private void SavePrefs()
|
||
{
|
||
EditorPrefs.SetInt(PrefPrefix + "decReducer", _decReducer);
|
||
EditorPrefs.SetInt(PrefPrefix + "decTris", _decTris);
|
||
EditorPrefs.SetInt(PrefPrefix + "decUnwrap", _decUnwrap);
|
||
EditorPrefs.SetFloat(PrefPrefix + "decMaxOverlap", _decMaxOverlap);
|
||
EditorPrefs.SetInt(PrefPrefix + "decQrCap", _decQrCap);
|
||
EditorPrefs.SetInt(PrefPrefix + "bakeSize", _bakeSize);
|
||
EditorPrefs.SetInt(PrefPrefix + "bakeSamples", _bakeSamples);
|
||
EditorPrefs.SetString(PrefPrefix + "bakeRayDist", _bakeRayDist);
|
||
EditorPrefs.SetInt(PrefPrefix + "maxSize", _maxSize);
|
||
EditorPrefs.SetString(PrefPrefix + "outSubDir", _outSubDir);
|
||
EditorPrefs.SetString(PrefPrefix + "blenderPath", _blenderPath);
|
||
EditorPrefs.SetString(PrefPrefix + "pythonPath", _pythonPath);
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
bool running = _proc != null;
|
||
|
||
using (new EditorGUI.DisabledScope(running))
|
||
{
|
||
PathField("Blender", ref _blenderPath, () =>
|
||
EditorUtility.OpenFilePanel("选择 blender.exe", "", "exe"), null);
|
||
PathField("Python", ref _pythonPath, () =>
|
||
EditorUtility.OpenFilePanel("选择 python.exe", "", "exe"), null);
|
||
}
|
||
|
||
GUILayout.Space(4);
|
||
_foldDec = EditorGUILayout.Foldout(_foldDec, "① 减面 (model_decimate)", true);
|
||
if (_foldDec) DrawDecimate(running);
|
||
|
||
GUILayout.Space(2);
|
||
_foldBake = EditorGUILayout.Foldout(_foldBake, "② 烘焙 (model_bake)", true);
|
||
if (_foldBake) DrawBake(running);
|
||
|
||
GUILayout.Space(2);
|
||
_foldConv = EditorGUILayout.Foldout(_foldConv, "③ 转换 XPbr (model_translator)", true);
|
||
if (_foldConv) DrawConvert(running);
|
||
|
||
GUILayout.Space(4);
|
||
if (running)
|
||
{
|
||
if (GUILayout.Button("取消", GUILayout.Height(24)))
|
||
{
|
||
KillProc();
|
||
Append("[已取消]");
|
||
}
|
||
double el = EditorApplication.timeSinceStartup - _procStartTime;
|
||
char spin = "|/-\\"[(int)(el * 6) % 4];
|
||
EditorGUILayout.HelpBox(string.Format(
|
||
"{0} {1}中… 已用时 {2:00}:{3:00}(Blender 处理大模型可能要几分钟,进度见下方日志)",
|
||
spin, _stageName, (int)(el / 60), (int)(el % 60)), MessageType.Info);
|
||
}
|
||
|
||
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||
EditorGUILayout.TextArea(_log.ToString(), GUILayout.ExpandHeight(true));
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawDecimate(bool running)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
using (new EditorGUI.DisabledScope(running))
|
||
{
|
||
FbxField("高模 FBX", ref _decHigh);
|
||
_decReducer = EditorGUILayout.Popup("减面后端", _decReducer, ReducerLabels);
|
||
_decTris = EditorGUILayout.IntField("目标三角面数", _decTris);
|
||
_decAdvanced = EditorGUILayout.Foldout(_decAdvanced, "高级");
|
||
if (_decAdvanced)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
_decUnwrap = EditorGUILayout.Popup("UV 展开", _decUnwrap, UnwrapLabels);
|
||
_decMaxOverlap = EditorGUILayout.FloatField(
|
||
new GUIContent("UV 重叠门限", "0-1,默认 0.08;调高可让更高 seam 角度过门、减少碎岛"),
|
||
_decMaxOverlap);
|
||
_decQrCap = EditorGUILayout.IntField(
|
||
new GUIContent("QR 预塌陷面数", "quad 后端 QR 前中等密度目标面数,默认 50000;高密度直接 QR 会破洞"),
|
||
_decQrCap);
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
}
|
||
RunButton("减面 →② 低模", ValidateDecimate(), running, StartDecimate);
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
|
||
private void DrawBake(bool running)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
using (new EditorGUI.DisabledScope(running))
|
||
{
|
||
FbxField("高模 FBX", ref _bakeHigh);
|
||
FbxField("低模 FBX", ref _bakeLow);
|
||
int si = Array.IndexOf(SizeOptions, _bakeSize);
|
||
if (si < 0) si = 2;
|
||
si = EditorGUILayout.Popup("贴图尺寸", si, SizeLabels);
|
||
_bakeSize = SizeOptions[si];
|
||
_bakeSamples = EditorGUILayout.IntField("AO 采样", _bakeSamples);
|
||
_bakeRayDist = EditorGUILayout.TextField(
|
||
new GUIContent("射线距离", "留空 = auto 自适应(推荐,文档最优);填数字则覆盖"), _bakeRayDist);
|
||
}
|
||
RunButton("烘焙 →③ FBX", ValidateBake(), running, StartBake);
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
|
||
private void DrawConvert(bool running)
|
||
{
|
||
EditorGUI.indentLevel++;
|
||
using (new EditorGUI.DisabledScope(running))
|
||
{
|
||
FbxField("FBX 文件", ref _fbxPath);
|
||
int idx = Array.IndexOf(SizeOptions, _maxSize);
|
||
if (idx < 0) idx = 2;
|
||
idx = EditorGUILayout.Popup("贴图最大尺寸", idx, SizeLabels);
|
||
_maxSize = SizeOptions[idx];
|
||
_outSubDir = EditorGUILayout.TextField(new GUIContent("输出子目录",
|
||
"Assets/Res/ 下的相对目录,留空=直接放 Res/<模型名>"), _outSubDir);
|
||
}
|
||
RunButton("转换 XPbr", ValidateConvert(), running, StartConvert);
|
||
EditorGUI.indentLevel--;
|
||
}
|
||
|
||
// 带"选择…"按钮、picker 固定为 FBX 的路径行(记住上次目录)
|
||
private void FbxField(string label, ref string value)
|
||
{
|
||
PathField(label, ref value,
|
||
() => EditorUtility.OpenFilePanel("选择 FBX",
|
||
EditorPrefs.GetString(PrefPrefix + "lastFbxDir", ""), "fbx"),
|
||
picked => EditorPrefs.SetString(PrefPrefix + "lastFbxDir",
|
||
Path.GetDirectoryName(picked)));
|
||
}
|
||
|
||
// 带"选择…"按钮的路径行;onPicked 可为 null
|
||
private static void PathField(string label, ref string value,
|
||
Func<string> pick, Action<string> onPicked)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
value = EditorGUILayout.TextField(label, value);
|
||
if (GUILayout.Button("选择…", GUILayout.Width(56)))
|
||
{
|
||
string p = pick();
|
||
if (!string.IsNullOrEmpty(p))
|
||
{
|
||
value = p.Replace('/', '\\');
|
||
onPicked?.Invoke(p);
|
||
GUI.FocusControl(null);
|
||
}
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
// 运行按钮 + 错误提示;err==null 才可点击
|
||
private void RunButton(string label, string err, bool running, Action start)
|
||
{
|
||
using (new EditorGUI.DisabledScope(running || err != null))
|
||
if (GUILayout.Button(label, GUILayout.Height(24)))
|
||
start();
|
||
if (err != null && !running)
|
||
EditorGUILayout.HelpBox(err, MessageType.Info);
|
||
}
|
||
|
||
// ---- 校验 ----
|
||
|
||
private string ValidateTools()
|
||
{
|
||
if (!File.Exists(_blenderPath))
|
||
return "未找到 blender.exe,请在上方指定路径";
|
||
// python 允许裸命令名(走 PATH),带目录时才检查存在
|
||
if ((_pythonPath.Contains("\\") || _pythonPath.Contains("/"))
|
||
&& !File.Exists(_pythonPath))
|
||
return "未找到 python,请安装 Python 3 或在上方指定路径";
|
||
return null;
|
||
}
|
||
|
||
private static string ValidateFbx(string p)
|
||
{
|
||
if (string.IsNullOrEmpty(p)) return "请选择 FBX 文件";
|
||
if (!File.Exists(p)) return "FBX 文件不存在:" + p;
|
||
if (!p.ToLowerInvariant().EndsWith(".fbx")) return "请选择 .fbx 文件";
|
||
return null;
|
||
}
|
||
|
||
private string ValidateDecimate()
|
||
{
|
||
return ValidateTools() ?? ValidateFbx(_decHigh)
|
||
?? (_decTris <= 0 ? "目标三角面数必须 > 0" : null);
|
||
}
|
||
|
||
private string ValidateBake()
|
||
{
|
||
return ValidateTools() ?? ValidateFbx(_bakeHigh) ?? ValidateFbx(_bakeLow);
|
||
}
|
||
|
||
private string ValidateConvert()
|
||
{
|
||
string e = ValidateTools() ?? ValidateFbx(_fbxPath);
|
||
if (e != null) return e;
|
||
string sub = _outSubDir.Replace('\\', '/').Trim('/', ' ');
|
||
if (sub.Contains("..") || Path.IsPathRooted(sub))
|
||
return "输出子目录必须是 Assets/Res 下的相对路径";
|
||
return null;
|
||
}
|
||
|
||
// ---- 三级启动 ----
|
||
|
||
private void StartDecimate()
|
||
{
|
||
string high = _decHigh;
|
||
string highName = Path.GetFileNameWithoutExtension(high);
|
||
// CLI -o 默认=源文件同目录,产物 <名>_low.fbx(见 model_decimate.py)
|
||
string lowOut = Path.Combine(Path.GetDirectoryName(high) ?? "", highName + "_low.fbx");
|
||
string reducer = _decReducer == 0 ? "quad" : "collapse";
|
||
string unwrap = _decUnwrap == 0 ? "seam" : "smart";
|
||
string args = string.Format(CultureInfo.InvariantCulture,
|
||
"\"{0}\" --tris {1} --reducer {2} --unwrap {3} --max-overlap {4} --qr-input-cap {5} --blender \"{6}\"",
|
||
high, _decTris, reducer, unwrap, _decMaxOverlap, _decQrCap, _blenderPath);
|
||
|
||
StartProcess("model_decimate.py", args,
|
||
"== 减面 " + Path.GetFileName(high) + " -> " + Path.GetFileName(lowOut),
|
||
"减面",
|
||
() =>
|
||
{
|
||
Append("[完成] " + lowOut);
|
||
if (File.Exists(lowOut))
|
||
{
|
||
_bakeHigh = high;
|
||
_bakeLow = lowOut;
|
||
_foldBake = true;
|
||
Append("[链式] 已填入②烘焙:高模 + 低模");
|
||
}
|
||
else
|
||
{
|
||
Append("[警告] 未找到减面产物 " + lowOut + ",请手动指定②低模");
|
||
}
|
||
});
|
||
}
|
||
|
||
private void StartBake()
|
||
{
|
||
string high = _bakeHigh, low = _bakeLow;
|
||
string lowName = Path.GetFileNameWithoutExtension(low);
|
||
// 镜像 bl_bake.output_stem:低模名去掉结尾 _low 作为输出前缀(对齐工具3命名约定)。
|
||
// 例:cityboy_low -> out_bake/cityboy/cityboy.fbx;supergirl_uv -> 原样保留
|
||
string stem = lowName.EndsWith("_low", StringComparison.Ordinal)
|
||
? lowName.Substring(0, lowName.Length - 4) : lowName;
|
||
// CLI -o 默认=out_bake,产物 out_bake/<stem>/<stem>.fbx(见 model_bake.py / output_stem)
|
||
string bakeOutFbx = Path.Combine(RepoRoot, "Tools", "ModelTranslator",
|
||
"out_bake", stem, stem + ".fbx");
|
||
var sb = new StringBuilder();
|
||
sb.AppendFormat("\"{0}\" \"{1}\" --size {2} --samples {3}",
|
||
high, low, _bakeSize, _bakeSamples);
|
||
string ray = (_bakeRayDist ?? "").Trim();
|
||
if (ray.Length > 0)
|
||
sb.AppendFormat(CultureInfo.InvariantCulture, " --ray-distance {0}", ray);
|
||
sb.AppendFormat(" --blender \"{0}\"", _blenderPath);
|
||
|
||
StartProcess("model_bake.py", sb.ToString(),
|
||
"== 烘焙 " + Path.GetFileName(low) + "(高模 " + Path.GetFileName(high) + ")",
|
||
"烘焙",
|
||
() =>
|
||
{
|
||
Append("[完成] " + bakeOutFbx);
|
||
if (File.Exists(bakeOutFbx))
|
||
{
|
||
_fbxPath = bakeOutFbx;
|
||
_foldConv = true;
|
||
Append("[链式] 已填入③转换 XPbr:FBX");
|
||
}
|
||
else
|
||
{
|
||
Append("[警告] 未找到烘焙产物 " + bakeOutFbx + ",请手动指定③FBX");
|
||
}
|
||
});
|
||
}
|
||
|
||
private void StartConvert()
|
||
{
|
||
string sub = _outSubDir.Replace('\\', '/').Trim('/', ' ');
|
||
string outRel = string.IsNullOrEmpty(sub) ? "Client/Assets/Res"
|
||
: "Client/Assets/Res/" + sub;
|
||
string modelName = Path.GetFileNameWithoutExtension(_fbxPath);
|
||
// CLI 会在 -o 下自动建 <模型名>/ 子目录(见 model_translator.py main)
|
||
string resultAssetDir = (string.IsNullOrEmpty(sub) ? "Assets/Res/" : "Assets/Res/" + sub + "/")
|
||
+ modelName;
|
||
string args = string.Format(
|
||
"\"{0}\" -o \"{1}\" --max-size {2} --blender \"{3}\"",
|
||
_fbxPath, Path.Combine(RepoRoot, outRel), _maxSize, _blenderPath);
|
||
|
||
StartProcess("model_translator.py", args,
|
||
"== " + Path.GetFileName(_fbxPath) + " -> " + resultAssetDir,
|
||
"转换",
|
||
() =>
|
||
{
|
||
Append("[完成] " + resultAssetDir);
|
||
AssetDatabase.Refresh();
|
||
var folder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(resultAssetDir);
|
||
if (folder != null)
|
||
{
|
||
Selection.activeObject = folder;
|
||
EditorGUIUtility.PingObject(folder);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 启动 <script> 子进程(python -u),成功(退出码0)时回调 onSuccess
|
||
private void StartProcess(string scriptRel, string args, string startLabel,
|
||
string stageName, Action onSuccess)
|
||
{
|
||
SavePrefs();
|
||
_stageName = stageName;
|
||
_procStartTime = EditorApplication.timeSinceStartup;
|
||
_lastAnimAt = _procStartTime;
|
||
string script = Path.Combine(RepoRoot, "Tools", "ModelTranslator", scriptRel);
|
||
var psi = new ProcessStartInfo
|
||
{
|
||
FileName = _pythonPath,
|
||
Arguments = string.Format("-u \"{0}\" {1}", script, args),
|
||
WorkingDirectory = RepoRoot,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
StandardOutputEncoding = Encoding.UTF8,
|
||
StandardErrorEncoding = Encoding.UTF8,
|
||
};
|
||
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; // CLI 输出中文
|
||
|
||
_log.Length = 0;
|
||
Append(startLabel);
|
||
_onSuccess = onSuccess;
|
||
try
|
||
{
|
||
_proc = Process.Start(psi);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_proc = null;
|
||
_onSuccess = null;
|
||
EditorUtility.DisplayDialog("模型转换",
|
||
"启动 python 失败:" + e.Message +
|
||
"\n请安装 Python 3 或在面板中指定 python.exe 路径", "确定");
|
||
return;
|
||
}
|
||
_proc.OutputDataReceived += (_, ev) => Enqueue(ev.Data);
|
||
_proc.ErrorDataReceived += (_, ev) => Enqueue(ev.Data);
|
||
_proc.BeginOutputReadLine();
|
||
_proc.BeginErrorReadLine();
|
||
}
|
||
|
||
private void Enqueue(string line)
|
||
{
|
||
if (line == null) return;
|
||
lock (_lock) _pending.Add(line);
|
||
}
|
||
|
||
private void Append(string line)
|
||
{
|
||
_log.AppendLine(line);
|
||
_scroll.y = float.MaxValue; // 滚到最底
|
||
}
|
||
|
||
private void OnUpdate()
|
||
{
|
||
bool dirty = false;
|
||
lock (_lock)
|
||
{
|
||
foreach (string l in _pending) { Append(l); dirty = true; }
|
||
_pending.Clear();
|
||
}
|
||
|
||
if (_proc != null && _proc.HasExited)
|
||
{
|
||
_proc.WaitForExit(); // 确保异步输出全部送达
|
||
lock (_lock) // 先冲掉剩余输出,保证 [完成]/[失败] 在日志最后
|
||
{
|
||
foreach (string l in _pending) Append(l);
|
||
_pending.Clear();
|
||
}
|
||
int code = _proc.ExitCode;
|
||
_proc.Dispose();
|
||
_proc = null;
|
||
var cb = _onSuccess;
|
||
_onSuccess = null;
|
||
dirty = true;
|
||
if (code == 0)
|
||
{
|
||
cb?.Invoke();
|
||
}
|
||
else
|
||
{
|
||
Append("[失败] 退出码 " + code);
|
||
EditorUtility.DisplayDialog("模型转换",
|
||
"处理失败(退出码 " + code + "),详见窗口日志", "确定");
|
||
}
|
||
double dt = EditorApplication.timeSinceStartup - _procStartTime;
|
||
Append(string.Format("[耗时 {0:00}:{1:00}]", (int)(dt / 60), (int)(dt % 60)));
|
||
}
|
||
|
||
// 运行中按 ~10fps 节流重绘,让计时器/spinner 动起来(即使暂无新日志)
|
||
if (_proc != null && EditorApplication.timeSinceStartup - _lastAnimAt > 0.1)
|
||
{
|
||
_lastAnimAt = EditorApplication.timeSinceStartup;
|
||
dirty = true;
|
||
}
|
||
|
||
if (dirty) Repaint();
|
||
}
|
||
|
||
private void KillProc()
|
||
{
|
||
if (_proc == null) return;
|
||
try
|
||
{
|
||
// 用 taskkill /T 连 blender 子进程一起杀(Mono BCL 无 Kill(tree) 重载)
|
||
if (!_proc.HasExited)
|
||
Process.Start(new ProcessStartInfo("taskkill", "/PID " + _proc.Id + " /T /F")
|
||
{ UseShellExecute = false, CreateNoWindow = true });
|
||
}
|
||
catch { /* 进程可能刚好退出 */ }
|
||
_proc.Dispose();
|
||
_proc = null;
|
||
_onSuccess = null;
|
||
lock (_lock) _pending.Clear(); // 丢弃残留输出,避免插在 [已取消] 之后
|
||
}
|
||
}
|
||
}
|
||
#endif
|