@jojonadam/dsh-blender 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ blender_mcp_exec.py — 经 Blender MCP 在运行中的 Blender 里执行任意 bpy 代码
5
+ =========================================================================
6
+ 随包分发版(v1.3.3)。零第三方依赖,只用标准库,macOS / Linux / Windows 通用。
7
+
8
+ 命令面已对上游核实(PyPI mcp-for-blender 2.2.2 的 --help 实测):
9
+ · `--host` / `--port` 真实存在,且帮助原文写「overrides BLENDER_HOST/BLENDER_PORT」
10
+ → 本脚本显式传 --port 即可,**不需要**再给子进程注入 BLENDER_PORT 环境变量。
11
+ · `install-addon` / `addon-paths` / `setup` / `update` 四个子命令真实存在。
12
+
13
+ 前置:
14
+ 1. Blender 已安装并**开着**(建议 5.2 LTS)
15
+ 2. 已装 uv(含 uvx):`pip install uv` 或见 https://docs.astral.sh/uv/
16
+ 3. Blender 内 N 面板 → MCP for Blender → **Start MCP Server**(默认端口 9876)
17
+ 插件没装/版本过旧 → 先跑 `uvx mcp-for-blender install-addon`
18
+
19
+ 用法:
20
+ python blender_mcp_exec.py path/to/script.py
21
+ echo "import bpy; print(len(bpy.data.objects))" | python blender_mcp_exec.py
22
+ python blender_mcp_exec.py script.py --timeout 300 # 重场景/渲染给足时间
23
+ python blender_mcp_exec.py script.py --port 9877 # Blender 插件换了端口
24
+
25
+ 退出码:0 有输出 / 1 连接或执行失败 / 2 用法错误(stderr 给原因,绝不静默挂死)
26
+ 归 2 的具体情形:脚本路径不存在、BLENDER_PORT 填错(非整数 / 空串 / 超 1-65535)、
27
+ 找不到 uvx —— 凡是「还没开始连 Blender 就已经确定跑不通」的都算用法错误,方便脚本判错。
28
+
29
+ --timeout 是**硬超时**:到点无条件退出并给诊断,不等子进程自然返回。
30
+ """
31
+ import argparse
32
+ import json
33
+ import os
34
+ import queue
35
+ import shutil
36
+ import signal
37
+ import subprocess
38
+ import sys
39
+ import threading
40
+ import time
41
+
42
+ DEFAULT_TIMEOUT = 90
43
+ DEFAULT_PORT = 9876
44
+ # 优先用新包名;blender-mcp 是官方兼容 wrapper,仍可用但会往 stderr 打迁移提示
45
+ PKG = "mcp-for-blender"
46
+
47
+ DIAG = (
48
+ " 1) Blender 是否开着?\n"
49
+ " 2) N 面板 → MCP for Blender → 是否点了 Start MCP Server?\n"
50
+ " 3) 先问一句 `get_addon_status`——它一句话就能说清插件在不在、版本对不对;\n"
51
+ " 版本不一致就 `uvx mcp-for-blender install-addon`,再到 Preferences → Add-ons\n"
52
+ " 里禁用再启用 'Interface: MCP for Blender'\n"
53
+ " 4) 若你改过端口,用 --port 指定实际端口(脚本当前用 {port})\n"
54
+ " 5) 插件装在哪个目录:`uvx mcp-for-blender addon-paths`"
55
+ )
56
+
57
+
58
+ def find_uvx():
59
+ """按 PATH → 常见安装位 → 报错。不写死任何一台机器的路径。"""
60
+ p = shutil.which("uvx")
61
+ if p:
62
+ return p
63
+ cands = [
64
+ os.path.expanduser("~/.local/bin/uvx"),
65
+ os.path.expanduser("~/.cargo/bin/uvx"),
66
+ "/opt/homebrew/bin/uvx",
67
+ "/usr/local/bin/uvx",
68
+ os.path.expanduser("~/AppData/Roaming/Python/Scripts/uvx.exe"),
69
+ os.path.expanduser("~/AppData/Local/Programs/uv/uvx.exe"),
70
+ ]
71
+ for c in cands:
72
+ if os.path.isfile(c):
73
+ return c
74
+ # 走 stderr + 退出码 2(用法/前置条件错),与 docstring 声明的退出码一致。
75
+ # 旧版 sys.exit("字符串") 实际返回 1,买家按 docstring 判码会判错。
76
+ sys.stderr.write(
77
+ "❌ 找不到 uvx。请先安装 uv:\n"
78
+ " macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n"
79
+ " Windows: powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\"\n"
80
+ " 或 pip install uv\n"
81
+ "装完确认 `uvx --version` 可用,再重跑本脚本。\n"
82
+ )
83
+ sys.exit(2)
84
+
85
+
86
+ def _port_from_env():
87
+ """读 BLENDER_PORT 并校验;不设该变量时返回 DEFAULT_PORT。
88
+
89
+ v1.3.1 的缺陷:`default=int(os.environ.get("BLENDER_PORT", DEFAULT_PORT))`
90
+ 在 argparse 构造期就 int(),环境变量为 `9876abc` 或空串时用户直接吃到
91
+ Python traceback(看不出是自己环境变量填错)。这里改成
92
+ 「先取值 → 校验 → 人话报错 + exit 2」。
93
+
94
+ 刻意**不静默回退默认端口**:静默回退会让用户以为连上了 9876,
95
+ 实际 Blender 开在别的端口,症状变成「连不上」而不是「变量填错」,更难查。
96
+ """
97
+ raw = os.environ.get("BLENDER_PORT")
98
+ if raw is None:
99
+ return DEFAULT_PORT
100
+ hint = (" 怎么办:要么删掉这个环境变量(回到默认端口 %d),"
101
+ "要么把它设成正确的整数端口号。\n" % DEFAULT_PORT)
102
+ s = raw.strip()
103
+ if not s:
104
+ sys.stderr.write(
105
+ "❌ 环境变量 BLENDER_PORT 是空字符串,不是端口号。\n" + hint)
106
+ sys.exit(2)
107
+ try:
108
+ v = int(s)
109
+ except ValueError:
110
+ sys.stderr.write(
111
+ "❌ 环境变量 BLENDER_PORT=%r 不是整数。\n" % raw + hint)
112
+ sys.exit(2)
113
+ if not 1 <= v <= 65535:
114
+ sys.stderr.write(
115
+ "❌ 环境变量 BLENDER_PORT=%r 超出端口范围(1-65535)。\n" % raw + hint)
116
+ sys.exit(2)
117
+ return v
118
+
119
+
120
+ class _Reader(threading.Thread):
121
+ """把 stdout 逐行读进队列,让主线程能带超时地等。
122
+
123
+ readline 本身是阻塞调用,直接在主线程 readline 会让 --timeout 形同虚设——
124
+ 这正是 v1.2 的缺陷:Blender 卡死时脚本无限挂住,外部强杀才结束。
125
+ """
126
+
127
+ def __init__(self, stream):
128
+ super().__init__(daemon=True)
129
+ self.stream = stream
130
+ self.q = queue.Queue()
131
+
132
+ def run(self):
133
+ try:
134
+ for line in self.stream:
135
+ self.q.put(line)
136
+ except Exception:
137
+ pass
138
+ finally:
139
+ self.q.put(None) # 哨兵:流结束
140
+
141
+
142
+ class _ErrReader(threading.Thread):
143
+ """stderr 同样必须异步读。若在主线程 read() 到 EOF,而孙进程还握着管道,
144
+ 就会二次挂死(v1.3 开发期实测踩到)。这里只累积,永不阻塞主线程。"""
145
+
146
+ def __init__(self, stream, limit=400):
147
+ super().__init__(daemon=True)
148
+ self.stream = stream
149
+ self.limit = limit
150
+ self.buf = ""
151
+
152
+ def run(self):
153
+ try:
154
+ for line in self.stream:
155
+ if len(self.buf) < self.limit:
156
+ self.buf += line
157
+ except Exception:
158
+ pass
159
+
160
+
161
+ def _stop(p):
162
+ """整组杀掉。uvx 会再起子进程,只 terminate 直接子进程不足以断开管道。
163
+
164
+ 分平台:
165
+ · POSIX:run_code 用 start_new_session=True 让 uvx 自成一个进程组,
166
+ 这里 killpg 整组收掉,孙进程不会漏。
167
+ · Windows:没有进程组可杀。`p.kill()` 只结束 uvx 本身,uvx 再起的
168
+ python / mcp 子进程会变孤儿并继续握着管道 → 优先 `taskkill /T /F`
169
+ (/T 连子孙进程树一起杀),失败或进程已退才退回 p.kill()。
170
+ """
171
+ try:
172
+ if os.name == "nt":
173
+ try:
174
+ subprocess.run(
175
+ ["taskkill", "/T", "/F", "/PID", str(p.pid)],
176
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
177
+ timeout=10, check=False,
178
+ )
179
+ except Exception:
180
+ pass
181
+ if p.poll() is None:
182
+ p.kill()
183
+ return
184
+ if hasattr(os, "killpg"):
185
+ os.killpg(os.getpgid(p.pid), signal.SIGKILL)
186
+ else:
187
+ p.kill()
188
+ except Exception:
189
+ try:
190
+ p.kill()
191
+ except Exception:
192
+ pass
193
+
194
+
195
+ def run_code(code, timeout=DEFAULT_TIMEOUT, port=DEFAULT_PORT):
196
+ cmd = [find_uvx(), PKG, "--port", str(port)]
197
+ kw = {}
198
+ if os.name == "posix":
199
+ kw["start_new_session"] = True # 让 killpg 能收掉 uvx 的整组子进程
200
+ try:
201
+ p = subprocess.Popen(
202
+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
203
+ text=True, encoding="utf-8", errors="replace", bufsize=1, **kw,
204
+ )
205
+ except FileNotFoundError as e:
206
+ return "ERROR: 无法启动 uvx " + PKG + " → " + str(e), 1
207
+
208
+ reader = _Reader(p.stdout)
209
+ ereader = _ErrReader(p.stderr)
210
+ reader.start()
211
+ ereader.start()
212
+
213
+ def send(msg):
214
+ try:
215
+ p.stdin.write(json.dumps(msg) + "\n")
216
+ p.stdin.flush()
217
+ except Exception:
218
+ pass
219
+
220
+ def read(deadline):
221
+ """硬超时:到点返回 'TIMEOUT',不等阻塞读自然返回。"""
222
+ while True:
223
+ remain = deadline - time.time()
224
+ if remain <= 0:
225
+ return "TIMEOUT"
226
+ try:
227
+ line = reader.q.get(timeout=remain)
228
+ except queue.Empty:
229
+ return "TIMEOUT"
230
+ if line is None:
231
+ return None # 子进程已退出
232
+ line = line.strip()
233
+ if not line:
234
+ continue
235
+ try:
236
+ return json.loads(line)
237
+ except Exception:
238
+ continue # 日志行 / 迁移提示,跳过继续等
239
+
240
+ def err_text():
241
+ _stop(p)
242
+ ereader.join(timeout=1.0)
243
+ return (ereader.buf or "").strip()
244
+
245
+ deadline = time.time() + timeout
246
+
247
+ send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
248
+ "params": {"protocolVersion": "2024-11-05", "capabilities": {},
249
+ "clientInfo": {"name": "blender-studio-exec", "version": "1.3.3"}}})
250
+ init = read(deadline)
251
+ if init is None or init == "TIMEOUT":
252
+ err = err_text()
253
+ return (f"CONNECT-FAIL({timeout}s 内没完成 MCP 握手)。逐条诊断:\n"
254
+ + DIAG.format(port=port) + f"\nstderr: {err}"), 1
255
+ if isinstance(init, dict) and "error" in init:
256
+ err_text()
257
+ return "ERROR: 握手失败 → " + json.dumps(init["error"], ensure_ascii=False), 1
258
+
259
+ server = (init.get("result", {}).get("serverInfo", {}) or {})
260
+ send({"jsonrpc": "2.0", "method": "notifications/initialized"})
261
+ send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
262
+ "params": {"name": "execute_blender_code", "arguments": {"code": code}}})
263
+ res = read(deadline)
264
+
265
+ if res == "TIMEOUT":
266
+ err = err_text()
267
+ return (f"TIMEOUT(硬超时 {timeout}s,已强制退出)。握手成功"
268
+ f"({server.get('name', '?')} {server.get('version', '?')}),"
269
+ f"但 execute_blender_code 没在时限内返回。\n"
270
+ f"常见原因:Blender 主线程被模态框/大渲染阻塞、插件服务端卡死,\n"
271
+ f"或脚本本身跑得久 → 渲染/模拟/大场景请加 --timeout 300 以上。\n"
272
+ + DIAG.format(port=port) + f"\nstderr: {err}"), 1
273
+ if res is None:
274
+ err = err_text()
275
+ return ("CONNECT-FAIL:服务进程提前退出。\n" + DIAG.format(port=port)
276
+ + f"\nstderr: {err}"), 1
277
+ if "error" in res:
278
+ err_text()
279
+ return "ERROR: " + json.dumps(res["error"], ensure_ascii=False), 1
280
+
281
+ txt = ""
282
+ for part in res.get("result", {}).get("content", []):
283
+ if part.get("type") == "text":
284
+ txt += part.get("text", "")
285
+ err_text()
286
+
287
+ # MCP 把「连不上 Blender」包装成正常文本返回,rc 会被误判为 0。
288
+ # 买家看到绿色的“跑完了”其实一次都没连上——必须翻成失败。
289
+ low = txt.lower()
290
+ if "could not connect to blender" in low or low.startswith("error executing code"):
291
+ return (f"CONNECT-FAIL:服务起来了,但连不上 Blender。\n"
292
+ f"服务端原话:{txt.strip()[:300]}\n" + DIAG.format(port=port)), 1
293
+ return txt, 0
294
+
295
+
296
+ def main():
297
+ ap = argparse.ArgumentParser(
298
+ description="经 Blender MCP 在运行中的 Blender 内执行 bpy 代码")
299
+ ap.add_argument("script", nargs="?", help="要执行的 .py 文件;省略则从 stdin 读")
300
+ ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT,
301
+ help=f"硬超时秒数(默认 {DEFAULT_TIMEOUT};渲染/模拟给 300+)")
302
+ ap.add_argument("--port", type=int, default=None,
303
+ help=f"Blender MCP 端口(默认 {DEFAULT_PORT};可用 BLENDER_PORT 环境变量覆盖)")
304
+ a = ap.parse_args()
305
+ # 显式 --port 优先;否则才去校验 BLENDER_PORT(校验失败 → 人话 + exit 2,不是 traceback)
306
+ port = a.port if a.port is not None else _port_from_env()
307
+
308
+ if a.script and os.path.exists(a.script):
309
+ with open(a.script, encoding="utf-8") as f:
310
+ code = f.read()
311
+ elif a.script:
312
+ # 走 stderr + 退出码 2(用法错误),与 docstring 一致;旧版 sys.exit(str) 实际返回 1
313
+ sys.stderr.write(
314
+ "❌ 文件不存在: %s\n"
315
+ " 检查路径拼写;或者省略脚本参数,改用 stdin 传代码:\n"
316
+ " echo \"import bpy; print(len(bpy.data.objects))\" | python blender_mcp_exec.py\n"
317
+ % a.script)
318
+ return 2
319
+ else:
320
+ if sys.stdin.isatty():
321
+ ap.print_help()
322
+ return 2
323
+ code = sys.stdin.read()
324
+
325
+ out, rc = run_code(code, a.timeout, port)
326
+ print(out)
327
+ return rc
328
+
329
+
330
+ if __name__ == "__main__":
331
+ sys.exit(main())
@@ -1,16 +1,12 @@
1
1
  ---
2
2
  name: blender-modeling-cookbook
3
- description: "Blender 建模技法库:参数化 bpy 构建、BMesh 操作、修改器栈、拓扑规则、UV 展开,以及让 low-poly 资产圆润的选择性 Bevel 配方。"
4
- whenToUse: "Blender 建模 / bpy / BMesh / 修改器 / 拓扑 / UV / Bevel 时"
3
+ description: "Blender 建模技法库:参数化 bpy 构建、BMesh 操作、修改器栈、拓扑规则、UV 展开、选择性 Bevel 配方(圆润 low-poly 来源)。锚定 5.2 LTS。"
4
+ whenToUse: "Blender 建模 / 渲染 / 导出 / 排障 / MCP 接线 时"
5
5
  license: MIT
6
6
  ---
7
7
 
8
-
9
8
  # 建模技法库(blender-modeling-cookbook)
10
9
 
11
- > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
-
13
-
14
10
  **沉淀实战建模技法,供 `blender-modeling` 与全员复用。** 锚定 Blender 5.2 LTS;语法不假设 4.x。
15
11
 
16
12
  ## §1 参数化 bpy 构建范式
@@ -49,7 +45,7 @@ license: MIT
49
45
 
50
46
  - **连续网格优于逐立方体**:地面/地形用一整张 heightfield,顶点色 splat 只在边界带、块中心纯色 → 无缝、色不糊。逐立方体各自顶点色无法跨 mesh 衔接(缝 + 色糊反模式)。
51
47
  - **低模代理**:quad 优先、拓扑干净、三角预算写死(tile ≤ 200、树 ≤ 1500)。
52
- - **重拓扑/UV/绑定必须人审**(StraySpark 硬限制 #1)。
48
+ - **重拓扑/UV/绑定必须人审**(三硬限制 #1)。
53
49
 
54
50
  ## §5 UV 展开
55
51
 
@@ -59,10 +55,20 @@ license: MIT
59
55
  ## §6 LOD 与导出
60
56
 
61
57
  - Decimate 生成 LOD(LOD1=50% / LOD2=75% / LOD3=90% 缩减)。
62
- - glTF/GLB 只出几何(5.2 丢色),颜色下游引擎套(见 `blender-5x-pitfalls` / `blender-sop` §4)。
58
+ - 顶点色须接进材质再导出(否则 `COLOR_0` 降为 ubyte),见 `blender-5x-pitfalls` 致命级 3 / `blender-sop` §4。
63
59
 
64
60
  ## §7 复用资产
65
61
 
66
62
  - `generate_3d`(tripo/hunyuan3d/hyper3d)云端造模独有道具 → 进 Blender 精修。
67
63
  - `search_assets` 直拉 Poly Pizza CC0 low-poly 作基线,对齐视觉语言后参数化自研。
68
- - helper:`/Users/jojo/AI_Projects/Seele/tools/blender_mcp_exec.py` 本地执行 bpy 脚本。
64
+ - helper:`scripts/blender_mcp_exec.py` 本地执行 bpy 脚本。
65
+
66
+ ## 无 Blender 时的参数交付
67
+
68
+ 建模跑不了,**参数照样必须给全** —— 按 `templates/model_spec.md` 填:
69
+
70
+ - 尺寸(X×Y×Z,米)、三角面上限、UV 通道数、材质槽数、LOD 级数
71
+ - 全局约定写死:+Y up 导出、变换必须应用、导出前三角化
72
+ - 材质与贴图:贴图尺寸、是否共用图集、**移动端尽量不用 Alpha Blend**
73
+
74
+ **参数给到能被照着做**,而不是"这个模型大概八百面左右"。
@@ -1,19 +1,50 @@
1
1
  ---
2
2
  name: blender-sop
3
- description: "Blender 专业标准:5.2 LTS 基线、Cycles/Eevee 抉择、CC0 资产红线、glTF/USD 导出纪律,以及 agent 驱动 Blender 协作的三条硬限制。所有管线判断的单一真相源。"
4
- whenToUse: "Blender / 渲染器选择 / Cycles / Eevee / CC0 / glTF / USD / 导出规范 时"
3
+ description: "Blender 工作室专业标准大脑:5.2 LTS 基线、Cycles/Eevee 抉择、CC0 资产红线、glTF/USD 导出纪律、Agent+MCP 协作三硬限制。一切管线判断的单一真相源。"
4
+ whenToUse: "Blender 建模 / 渲染 / 导出 / 排障 / MCP 接线 时"
5
5
  license: MIT
6
6
  ---
7
7
 
8
-
9
8
  # Blender 专业标准(blender-sop)
10
9
 
11
- > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
10
+ > **DSH 适配**:本技能来自专家团 v1.3.6。文中的「主理人」= 你本人,「成员 / 派单」= 你的子任务或子代理,「回执」= 你的交付记录。单 agent 场景下这些纪律同样成立。
12
11
 
13
12
 
14
13
  **所有管线判断以此为准。** 本技能是 Blender 工作室的"大脑",版本/渲染器/资产/导出/协作纪律的单一真相源。
15
14
 
16
- ## §1 版本基线(务必写准,别犯 Godot 的版本错)
15
+ ## §0 没有 Blender 时的标准交付(先给东西,再要东西)
16
+
17
+ **没装 Blender、MCP 连不上、或用户只想先看方案时,本团不是"等环境",而是先把能用的东西交出去。**
18
+
19
+ 按 `templates/` 下四份表填写,**填完即为可交产物,不需要打开 Blender**:
20
+
21
+ | 模板 | 填什么 | 交付形态 |
22
+ |---|---|---|
23
+ | `templates/asset_list.md` | 资产清单:资产名 / 类型 / 用途 / 面数预算 / 命名 | md(可交给建模的人直接开工) |
24
+ | `templates/model_spec.md` | 建模参数:尺寸(米)/ 三角面上限 / UV 通道 / 材质槽 / LOD | md |
25
+ | `templates/naming_export.md` | 命名前缀 + **glTF 导出参数**(+Y up / 单位 / 三角化 / 切线 / 动画)+ 目录结构 | md |
26
+ | `templates/delivery_checklist.md` | 交付检查表:命名 / 面数 / UV / 材质 / **导出可回读** / 预览图 | md |
27
+
28
+ 另有两份**工序单**(做 UV / 布光时照单执行,填写同样不需要 Blender):
29
+
30
+ | 模板 | 填什么 | 交付形态 |
31
+ |---|---|---|
32
+ | `templates/uv_unwrap.md` | UV 展开打包工序:标记缝合边 → 展开 → 打包优化;**打包率验收线 0.75** | md(工序单+实测值) |
33
+ | `templates/lighting_exposure.md` | 布光/曝光工序:三点布光 → 曝光校准 → 观感自检(复用 C4 成品渲染配方) | md(工序单+实测值) |
34
+
35
+ **纪律(四条)**:
36
+
37
+ 1. **不许只回「请先安装 Blender」就结束** —— 那是把工作推回给买家,而他要的是方案。
38
+ 2. **不许编造"已建模 / 已渲染 / 已导出"** —— 没连上就不说做了,但**方案必须交付**。
39
+ 3. **四份表跨表咬合检查(交付前必跑,可执行步骤)**:以 `asset_list` 为基准表,与 `model_spec` / `naming_export` / `checklist`(含成员并行产出的表)逐资产核对**七项字段**:资产名(逐字)/ 尺寸(米)/ 面数预算(目标+上限)/ UV 通道 / 材质名 / 导出格式 / 交付形态(主件与备件)。机器检查:`python3 scripts/check_table_consistency.py <表A> <表B>`(exit 0 且无输出 = 咬合;有输出 = 矛盾清单,每条带文件行号);表单自由到机器查不动时,人肉对同七项。**任何一项不一致 = 不许宣称「表已咬合」**,处置只有两种:改到一致,或标「待拍板」挂给老板拍板。没跑过这项检查,禁止使用「基本咬合」「只剩一处遗留」这类状态词。
40
+ 4. **共同前提卡(并行派单前必锁)**:并行给两个及以上成员派单前,先发**共同前提卡**锁死 **4 个数字:资产名 / 尺寸(米)/ 面数预算(目标+上限)/ 导出格式**,随派单一起下发。成员回报必须逐项回显这 4 个数字;任何成员想偏离其中任一项,必须先请示主理人转老板拍板,**不许自改**。实机测试(2026-11-02)暴露的教训:无此卡时同批表会出现尺寸 0.6/0.8、上限 ≤800/≤400、UV0/UV0+UV1、材质名带不带 `_01`、FBX/GLB 主件各写各的——5 处矛盾全因此而起。
41
+
42
+ **自检**:`python3 scripts/check_templates.py --selftest`(验四份齐全、表头与示例对齐、顶部声明、SKILL 已引用)。
43
+
44
+ **装好 Blender + MCP 之后**:这四份表就是施工图 —— 按 `asset_list` 建对象、按 `model_spec` 控参数、
45
+ 按 `naming_export` 命名与导出、用 `delivery_checklist` 逐项验收。
46
+
47
+ ## §1 版本基线(务必写准版本号)
17
48
 
18
49
  - **最新稳定**:Blender **5.2 LTS**(2026-09-15 发布,支持到 2028-07)。本机已装 **5.2.1 LTS**(`/Applications/Blender.app`)。
19
50
  - **次选长期支持**:4.5 LTS(2025-07,支持到 2027-07)。4.2 LTS 已于 2026-07 EOL。
@@ -38,13 +69,13 @@ license: MIT
38
69
 
39
70
  ## §4 导出纪律(glTF/USD/FBX)
40
71
 
41
- - **glTF/GLB**:游戏/AR 默认。本环境 **5.2 glTF 导出器丢色**(顶点色→白色 `COLOR_0`+畸形 `COLOR_1`;纯色材质→无 `pbrMetallicRoughness`)→ **GLB 只出几何,颜色在下游引擎侧套**(Godot/Maya/Unity 建材质)。
72
+ - **glTF/GLB**:游戏/AR 默认。**顶点色默认导出会降位**(`COLOR_0` 变 ubyte + 多出畸形 `COLOR_1`),根因是顶点色没被材质节点树引用;**解法:把顶点色接进材质(ShaderNodeVertexColor → Base Color)或导出时 `export_vertex_color='ACTIVE'`**。精确控色要求高时,仍可「GLB 只出几何、颜色到引擎侧套」(最稳,非唯一解)。纯色材质实测 `pbrMetallicRoughness` 正常,不受影响。**能不能不自认倒霉请先自测** → `blender-5x-pitfalls` 致命级第 3 条附 10 秒自检脚本。
42
73
  - **FBX**:Maya/Unity 互通。
43
74
  - **USD**:Blender→Unreal 一键管线(多 agent 跨软件)。
44
75
  - **Decimate** 生成 LOD(LOD1=50% / LOD2=75% / LOD3=90% 缩减)。
45
76
  - 导出参数(5.x):`export_format='GLB'`、`export_materials='EXPORT'`(**无 `export_colors` 参数**,5.x 已移除)。
46
77
 
47
- ## §5 Agent+MCP 协作三硬限制(StraySpark)
78
+ ## §5 Agent+MCP 协作三硬限制
48
79
 
49
80
  1. **创意拓扑不是 agent 的活**:概念/大形/风格/轮廓决策由**人**做;agent 只接手可参数化、可验证子任务(blockout / 低模代理)。手动重拓扑/UV/绑定/动画必须人审。
50
81
  2. **重操作超时 60–120s**:细分/重拓扑/烘焙/模拟 bake 会超时——须拆小步、降分辨率、或交人手动在 Blender 内跑。
@@ -52,13 +83,47 @@ license: MIT
52
83
 
53
84
  ## §6 Blender-MCP 协作铁律
54
85
 
55
- - **连接前提**:Blender 必须开着且插件服务端已启动(N 面板 → BlenderMCP → Start MCP Server)。无连接证明(如 `get_addon_status` 已连)前,任何成员不得声称能操控 Blender。
86
+ - **连接前提**:Blender 必须开着且插件服务端已启动(N 面板 → `MCP for Blender`(旧版显示为 BlenderMCP)→ Start MCP Server)。无连接证明(如 `get_addon_status` 已连)前,任何成员不得声称能操控 Blender。
56
87
  - **执行后必 `look` 自检**:`look` 是 agent 唯一视觉通道,是硬限制 #3 的补偿机制。
57
88
  - **代码即资产**:结构化操作优先落 bpy 脚本(`execute_blender_code`),可版本化、可 diff、可复现;LL3M 自批判循环 = 执行 → 读回 → 修正。
58
- - **工具按需加载**:只亮少量核心工具(get_addon_status / get_scene_info / execute_blender_code / look),重工具经 `search_assets`/`generate_3d` 调用,别全量塞(复用 Godot 团"131 工具全量注入→幻觉"教训)。
89
+ - **工具按需加载**:只亮少量核心工具(get_addon_status / get_scene_info / execute_blender_code / look),重工具经 `search_assets`/`generate_3d` 调用,别全量塞(工具全量注入会显著抬高小模型的幻觉率)。
59
90
  - **造模上云、精修在本地**:`generate_3d`(tripo/hunyuan3d/hyper3d)云端造模绕开本地重算超时(硬限制 #2);精修留本地 bpy。
60
91
  - **隐私**:默认本地优先(127.0.0.1);云端造模经 `uvx` 运行时,须显式告知用户数据出站。
61
92
 
62
93
  ## §7 修改与同步纪律
63
94
 
64
95
  本技能为团队标准基线。每次建模/渲染发现的新技法/坑/规范 → 回写本 SKILL.md 对应章节 + MEMORY.md(项目级)。不擅自推翻 §1–§6 级红线;涉及决策变更先与主理人(艾芙琳)对齐。
96
+
97
+ ## §8 工序条目(建模/雕刻/导出前的硬工序)
98
+
99
+ > 以下工序是**非过不可的步骤**,不是建议。每条都带可断言的验收线——做完必须自检数字,不许「看起来没问题」。
100
+
101
+ ### §8.1 雕刻基底密度门槛
102
+
103
+ - **雕刻/有机形基底一律高细分球**:`u_segments=128 × v_segments=96`(≈1.2 万顶点),不许用默认球(默认 32×16 只有 2562 顶点,撑不住雕刻级形变)。
104
+ - 断言:基底顶点数 **≥1 万**再开始形变。
105
+
106
+ ### §8.2 导出前冻结变换 + 应用修改器
107
+
108
+ - 导出前必做:`bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)` + `modifier_apply` 逐个应用栈内修改器。
109
+ - 断言:回读尺寸差 **= 0.0**(不冻结时实测漂移 8.2%)。
110
+
111
+ ### §8.3 布尔/倒角后拓扑卫生
112
+
113
+ - `boolean` / `bevel` 后必做三件:`bmesh.ops.remove_doubles(dist=1e-5)` → `bmesh.ops.recalc_face_normals` → **winding 冲突断言 = 0**(两面共享边被同向遍历即冲突)。
114
+ - 断言:winding 冲突 0、signed volume > 0。
115
+
116
+ ### §8.4 修改器栈组合后断言流形
117
+
118
+ - 多修改器(ARRAY/MIRROR 等)组合后,取**求值网格**(`evaluated_depsgraph_get`)断言非流形边 = 0;重叠实例处先焊接(remove_doubles)再断言。
119
+ - 重叠缝不焊 → 求值网格非流形边暴涨(实测 1728 条)。
120
+
121
+ ### §8.5 旋成体封口规范
122
+
123
+ - 旋成(spin)封口用 **spin + merge**(`use_merge=True` + `remove_doubles`),**禁止裸 ngon 封口**——裸 ngon 被细分后在盖面内部产生大价极点。
124
+ - 断言:大价极点(价 >8)只允许出现在**旋转轴上**,轴外大价极点 = 0。
125
+
126
+ ### §8.6 对称雕刻镜像约束
127
+
128
+ - 对称形体的形变权重**只用 `|x|`**(或镜像修改器约束),禁止左右各算一套。
129
+ - 断言:镜像配对顶点最大偏差 **< 1e-3**(理想 1e-6)。