@jojonadam/dsh-blender 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JOJO & Adam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
1
+ # dsh-blender · JOJO&Adam
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Blender 在 DeepSeek Harness 里的生产包:headless 跑 bpy 脚本、渲染、导出 glTF,外加建模技法、5.x 踩坑和程序化资产管线。
4
+
5
+ 不是“教你怎么点 Blender”,是让 agent 真的能把场景建出来、渲染出来、导出成游戏资产。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ # 从 npm
11
+ dsh plugin --profile web add @jojonadam/dsh-blender # Web 版
12
+ dsh plugin --profile desktop add @jojonadam/dsh-blender # 桌面版
13
+
14
+ # 本地开发
15
+ dsh plugin --profile desktop add link:/absolute/path/to/dsh-blender
16
+ ```
17
+
18
+ ## 30 秒上手
19
+
20
+ 前置:本机装有 Blender 5.2 LTS(macOS 默认 `/Applications/Blender.app`)。
21
+
22
+ ```bash
23
+ # 1. 体检
24
+ python3 skills/blender-studio/scripts/blender_client.py doctor
25
+
26
+ # 2. 冒烟渲染(默认立方体,256×256)
27
+ python3 skills/blender-studio/scripts/blender_client.py smoke --res 256
28
+
29
+ # 3. 跑示例:参数化地砖 → 渲染 → 保存 .blend → 导出 .glb
30
+ python3 skills/blender-studio/scripts/blender_client.py run \
31
+ --script skills/blender-studio/examples/make_tiles.py \
32
+ --outdir outputs/blender/tiles
33
+ ```
34
+
35
+ ## 包含
36
+
37
+ | 技能 | 用途 |
38
+ |---|---|
39
+ | `blender-studio` | 总入口:体检、headless 执行、渲染、导出(含客户端与示例) |
40
+ | `blender-env` | 找不到 Blender、版本、METAL、黑图、权限、bpy 报错 |
41
+ | `blender-sop` | 5.2 基线、Cycles/EEVEE 抉择、CC0 红线、导出纪律 |
42
+ | `blender-modeling-cookbook` | 参数化建模、BMesh、修改器栈、拓扑、UV、选择性 Bevel |
43
+ | `blender-5x-pitfalls` | 5.x 的 API 变更与已知坑 |
44
+ | `blender-asset-pipeline` | 程序化资产、批量、LOD、glTF 导出、连续网格反模式 |
45
+ | `blender-mcp-bridge` | 需要实时操控打开着的 Blender 时接 MCP |
46
+
47
+ ## 设计边界
48
+
49
+ - 默认 headless:确定性任务(建模/材质/渲染/导出)可复现;
50
+ - 雕刻、手动绑定、视觉微调交给 MCP 或人;
51
+ - 不代装 Blender、不代下资产(CC0 规则见 SOP);
52
+ - 只在本机执行,不经过云端。
53
+
54
+ ## 验证
55
+
56
+ ```bash
57
+ node test/provider.test.mjs
58
+ python3 skills/blender-studio/scripts/blender_client.py doctor
59
+ ```
60
+
61
+ 实机验证:Blender 5.2.1 LTS · macOS · METAL,EEVEE 256×256 冒烟 3.5s;参数化地砖(9 块 + 选择性 Bevel + glTF)3.6s(2026-10-19)。
62
+
63
+ ## 品牌
64
+
65
+ JOJO & Adam · <https://jojo-adam.com>
66
+
67
+ ## License
68
+
69
+ MIT
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: jojo-adam-blender
3
+ name: '@jojonadam/dsh-blender'
package/icon.svg ADDED
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="dsh-blender">
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="96" y2="96" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#f59e0b"/>
5
+ <stop offset="1" stop-color="#3b82f6"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="96" height="96" rx="22" fill="#0f1117"/>
9
+ <g fill="none" stroke="url(#g)" stroke-width="4" stroke-linejoin="round">
10
+ <path d="M48 18 78 34 48 50 18 34Z"/>
11
+ <path d="M18 34v28l30 16V50"/>
12
+ <path d="M78 34v28L48 78"/>
13
+ </g>
14
+ </svg>
package/lib/index.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @jojonadam/dsh-blender — bundled skill provider.
3
+ *
4
+ * Discovers every skill bundle under `skills/<name>/SKILL.md` and serves it to
5
+ * the DSH skill registry, with the skill directory as its resource base so
6
+ * `scripts/` and `examples/` resolve normally.
7
+ *
8
+ * @module @jojonadam/dsh-blender
9
+ */
10
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
11
+ import { basename, dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ const PROVIDER_NAME = 'jojo-adam-blender'
15
+ const SKILLS_DIR = fileURLToPath(new URL('../skills/', import.meta.url))
16
+ /** Same rank as other bundled skill roots (dsh-skill-filesystem BUNDLED_SKILL_RANK). */
17
+ const BUNDLED_SKILL_RANK = 600
18
+
19
+ /** Split `---` YAML frontmatter from the body. Handles single-line quoted values. */
20
+ function parseFrontmatter(text) {
21
+ if (!text.startsWith('---')) return { data: {}, body: text }
22
+ const end = text.indexOf('\n---', 3)
23
+ if (end === -1) return { data: {}, body: text }
24
+ const raw = text.slice(3, end).trim()
25
+ const body = text.slice(end + 4).replace(/^\r?\n/, '')
26
+ const data = {}
27
+ for (const line of raw.split(/\r?\n/)) {
28
+ const match = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line)
29
+ if (match === null) continue
30
+ let value = match[2].trim()
31
+ if (
32
+ (value.startsWith('"') && value.endsWith('"')) ||
33
+ (value.startsWith("'") && value.endsWith("'"))
34
+ ) {
35
+ value = value.slice(1, -1)
36
+ }
37
+ data[match[1]] = value
38
+ }
39
+ return { data, body }
40
+ }
41
+
42
+ function skillDirectories() {
43
+ let entries
44
+ try {
45
+ entries = readdirSync(SKILLS_DIR, { withFileTypes: true })
46
+ } catch {
47
+ return []
48
+ }
49
+ return entries
50
+ .filter((entry) => entry.isDirectory())
51
+ .map((entry) => join(SKILLS_DIR, entry.name))
52
+ .filter((dir) => {
53
+ try {
54
+ return statSync(join(dir, 'SKILL.md')).isFile()
55
+ } catch {
56
+ return false
57
+ }
58
+ })
59
+ }
60
+
61
+ function readSkill(dir) {
62
+ const locator = join(dir, 'SKILL.md')
63
+ const { data, body } = parseFrontmatter(readFileSync(locator, 'utf8'))
64
+ const name = data.name ?? basename(dir)
65
+ return {
66
+ candidate: {
67
+ name,
68
+ description: data.description ?? '',
69
+ ...(data.whenToUse === undefined ? {} : { whenToUse: data.whenToUse }),
70
+ invocation: { modelInvocable: true, userInvocable: true },
71
+ provider: PROVIDER_NAME,
72
+ source: 'bundled',
73
+ resourceBase: { kind: 'directory', path: dir },
74
+ rank: BUNDLED_SKILL_RANK,
75
+ locator,
76
+ },
77
+ content: body,
78
+ }
79
+ }
80
+
81
+ const provider = {
82
+ name: PROVIDER_NAME,
83
+ async list() {
84
+ return skillDirectories().map((dir) => readSkill(dir).candidate)
85
+ },
86
+ async get(candidate) {
87
+ const loaded = readSkill(dirname(candidate.locator))
88
+ return {
89
+ name: loaded.candidate.name,
90
+ description: loaded.candidate.description,
91
+ ...(loaded.candidate.whenToUse === undefined
92
+ ? {}
93
+ : { whenToUse: loaded.candidate.whenToUse }),
94
+ invocation: loaded.candidate.invocation,
95
+ provider: PROVIDER_NAME,
96
+ source: 'bundled',
97
+ resourceBase: loaded.candidate.resourceBase,
98
+ content: loaded.content,
99
+ }
100
+ },
101
+ }
102
+
103
+ /** Cordis plugin name. */
104
+ const name = 'dsh-blender'
105
+ /** Service required by the bundled provider. */
106
+ const inject = ['skills']
107
+
108
+ /** Register the bundled skill provider on `ctx.skills`. */
109
+ function apply(ctx) {
110
+ ctx.skills.registerProvider(() => provider)
111
+ }
112
+
113
+ export { apply, inject, name }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@jojonadam/dsh-blender",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Blender production pack for DeepSeek Harness: headless bpy runner, render/export helpers, modeling cookbook, 5.x pitfalls and procedural asset pipeline. 本地建模 · 零依赖客户端 · 5.2 LTS 校准。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./cordis.patch.yml": "./cordis.patch.yml",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "skills",
15
+ "cordis.patch.yml",
16
+ "icon.svg",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "dsh-plugin",
22
+ "deepseek-harness",
23
+ "blender",
24
+ "bpy",
25
+ "3d",
26
+ "gltf",
27
+ "procedural-assets"
28
+ ],
29
+ "license": "MIT",
30
+ "author": "JOJO & Adam",
31
+ "homepage": "https://jojo-adam.com",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/JOJO-Adam/dsh-blender.git"
35
+ },
36
+ "engines": {
37
+ "dsh": ">=0.2.0-rc.2",
38
+ "node": ">=20"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/JOJO-Adam/dsh-blender/issues"
45
+ },
46
+ "peerDependencies": {
47
+ "@deepseek-ai/cordis": "~4.0.5"
48
+ },
49
+ "dsh": {
50
+ "bundle": {
51
+ "patch": "./cordis.patch.yml"
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: blender-5x-pitfalls
3
+ description: "Blender 5.x 踩坑清单(锚定 5.2 LTS):Bevel 引用与限角陷阱、bevel_weight 移除、材质节点创建、Principled 无 Transmission、glTF 丢色 bug、transform_apply 方向陷阱。"
4
+ whenToUse: "Blender 5.x / 版本升级 / Bevel / 材质 / glTF 导出 / transform_apply 时"
5
+ license: MIT
6
+ ---
7
+
8
+
9
+ # Blender 5.x 踩坑清单(blender-5x-pitfalls)
10
+
11
+ > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
+
13
+
14
+ **锚定 5.2 LTS。** 本环境已踩过的真实 bug,建脚本前先查,避免重蹈。详见 `blender-sop` / `blender-asset-pipeline` / `blender-modeling-cookbook`。
15
+
16
+ ## 🔴 致命级(必踩无疑,先读)
17
+
18
+ 1. **Bevel 引用**:用 `obj.modifiers.new(name="Bevel", type='BEVEL')` 拿引用,不用 `bpy.ops.object.modifier_add(type='BEVEL')` 后按名 `obj.modifiers["Bevel"]` 取 —— 5.x 报 `key "Bevel" not found`。
19
+ 2. **Bevel 必须限角**:`limit_method='ANGLE'` + `angle_limit`(如 40°)。默认 `NONE` 会把**所有边(含平顶内部网格线)全倒角** → 平顶重引缝。限角后只磨圆崖壁/外缘(90°),平顶 0° 不动。
20
+ 3. **glTF 色导出 bug(5.2)**:顶点色→白色 `COLOR_0`(ubyte)+畸形 `COLOR_1`(ushort);纯色材质→无 `pbrMetallicRoughness`。**对策:GLB 只出几何,颜色全在下游引擎侧套。** `export_scene.gltf` **无 `export_colors` 参数**(5.x 已移除)→ 用 `export_format='GLB', export_materials='EXPORT'`。
21
+ 4. **`transform_apply(location=True)` 把 `obj.location` 归零**:apply 之后用 `obj.location.x/y` 判方向会符号全翻 → 整块所有竖直棱被误选。**改用顶点包围盒中心** `0.5*(min+max)` 判方向。
22
+
23
+ ## 🟠 高频级
24
+
25
+ 5. **`MeshEdge.bevel_weight` 已移除**(5.x):改成网格属性 `bevel_weight_edge`,但 Bevel `limit_method='WEIGHT'` 实测 flaky。最稳 = `bmesh.ops.bevel(geom=[选中棱], affect='EDGES', ...)` 直接倒角,版本无关。
26
+ 6. **新材质无自动命名**:`node_tree.nodes.clear()` 后显建 Output + BSDF。
27
+ 7. **`ColorRamp` element `.color` 需 RGBA 四元组**(4 个分量)。
28
+ 8. **5.x Principled BSDF 无 `Transmission` 输入**:透明水面只调 `blend_method='BLEND'` + Alpha,别找 Transmission 插槽。
29
+ 9. **Eevee 已重写(EEVEE-Next)**:不假设 4.x 旧 Eevee 行为;灯光/材质节点差异。
30
+ 10. **`look` 回传被过滤**:部分环境 "model does not support images" → agent 无法视觉自检,靠客观 DIAG/PROOF(法线分布/坐标/渐变顶点数)+ 用户眼睛终审。
31
+
32
+ ## 🟡 架构级(反模式)
33
+
34
+ 11. **逐立方体各自顶点色 = 缝 + 色糊**:地面/地形必须用一整张连续 heightfield,顶点色 splat 只在边界带、块中心纯色。
35
+ 12. **bmesh 跨会话 bevel 报 `BMEdge has been removed`**:选中+bevel+to_mesh+free 必须在**单 bmesh 会话内**完成。
36
+ 13. **过渡名无拓扑后缀互相覆盖**:加 `_inland/_side/_corner` 区分。
37
+
38
+ ## 关联
39
+
40
+ - 版本/渲染器/导出纪律总纲 → `blender-sop` §1/§2/§4。
41
+ - 连续网格 + Bevel 算法 + 选择性倒角 → `blender-asset-pipeline` / `blender-modeling-cookbook` §3–§5。
42
+ - 工具名对齐(mcp-for-blender vs 旧 blender-mcp)→ `blender-mcp-bridge`。
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: blender-asset-pipeline
3
+ description: "Agent 驱动的程序化资产管线:先设计再动手,AI 写参数化 bpy 脚本 + 自检 + glTF 导出,人负责美术方向。覆盖三硬限制、Bevel 圆角、CC0 拉取、LOD 导出与连续网格反模式。"
4
+ whenToUse: "Blender 资产管线 / 程序化生成 / 游戏资产 / glTF / LOD / 批量 时"
5
+ license: MIT
6
+ ---
7
+
8
+
9
+ # Blender 资产管线 · 设计优先的协作建模
10
+
11
+ > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
+
13
+
14
+ **先设计,再动手;代码即资产表征,用户定美术方向。**
15
+
16
+ 本技能把「Agent 用 Blender 建模」的圈内先进经验固化为可复用流程。核心思想来自 LL3M(arXiv 2508,把 3D 生成重定义为写可解释 bpy 代码)、AAA Blender Modeling Playbook(参数化优先 + 精修循环)、StraySpark(数百小时实战的三条硬限制)。**本版已通用化**:剥离了原 Seele 小岛项目的 6×6 演示夹具、TYPE_MAP/HEIGHT_MAP 硬编码、Godot 侧上色段落与项目红线,仅保留可复用的技法与纪律。
17
+
18
+ ## 适用场景
19
+
20
+ - 为游戏 / AR 批量生成 low-poly / 圆润风格 3D 模块:地形 tile、props(树/灌丛/礁石/小屋)、角色占位。
21
+ - 已接 Blender MCP(mcp-for-blender),想用 agent 脚本化批量建模,而非手动点菜单。
22
+ - 要把程序化方块占位升级为圆润 low-poly 视觉(逃离 Minecraft 感)。
23
+ - 需要「参考图 → Spec → 参数化生成 → 截图检视 → 导出 → 引擎实测」的闭环。
24
+
25
+ ## 核心原则:先设计,再动手
26
+
27
+ 绝不在没有 brief/spec 的情况下动网格。每个资产走:
28
+
29
+ ```
30
+ 简报&参考 → Spec 表(锚定数值) → 参数化构建 → 截图检视 → 导出 → 引擎实测 → 修订
31
+ ```
32
+
33
+ - **Reference-grounded**:动手前定 1–3 张参考图(动森/朋友生活/GoGoTown 截图,或 Poly Pizza CC0 模型,或用户给的方向)。无参考不建模。
34
+ - **Spec 表锚定数值**:尺寸(如 1×1 grid 单位)、三角预算(tile ≤ 200、树 ≤ 1500)、配色、pivot。数值写死 → agent 行为确定、可复现。
35
+ - **参数化 > 生成式 > 基元**:优先用参数化库/脚本;外部 AI 生成(`generate_3d` / Hunyuan3D)次之;纯基元 blockout 仅做占位。
36
+
37
+ ## 三条硬限制(必守,来自 StraySpark)
38
+
39
+ 1. **创意拓扑不是 agent 的活**:LLM 出不了生产级拓扑。agent 只出 blockout / 占位 / low-poly 代理;圆润比例、轮廓、配色由用户定。手动重拓扑/UV/绑定/动画必须人审。
40
+ 2. **重操作会超时**:细分(Subsurf)、重拓扑、纹理烘焙可能超过 MCP 工具超时(通常 60–120s)。大任务拆小步;或手动跑长任务。
41
+ 3. **agent 视觉反馈有限**:靠 `look` 渲图 / `get_scene_info` 拓扑事实,而非自己"看到"渲染。最终裁判是**用户眼睛 + 游戏引擎实跑**,不是 agent 自认。
42
+
43
+ ## 标准闭环(Phase A–E)
44
+
45
+ | 阶段 | 做什么 | 谁 | 出口 |
46
+ |---|---|---|---|
47
+ | A 简报+参考 | 定 1–3 张参考图 + 写 Spec 表(尺寸/三角预算/配色/pivot) | 用户给方向 | Spec 表 |
48
+ | B 参数化构建 | bpy + Bevel 生成 low-poly,或 `generate_3d` 拉云端模型进来精修 | agent | .blend |
49
+ | C 截图检视 | `look` 渲 turntable/接触表,对照 Spec 自检 3 个最高影响问题 | agent | 截图+缺陷清单 |
50
+ | D 导出 | glb → 资产目录,apply transform + 可选 Decimate LOD | agent | .glb(几何-only) |
51
+ | E 引擎实测 | 引擎内替换占位、跑场景看比例/锚点/视觉 | 用户+agent | 修订项 |
52
+
53
+ > ⚠️ 工具名对齐 mcp-for-blender:阶段 C 用 `look`(非旧版 `get_viewport_screenshot`);阶段 B 云端造模用 `generate_3d`(非旧版 `generate_hyper3d_model`);CC0 直拉用 `search_assets`(非旧版 `get_polyhaven_assets`)。
54
+
55
+ ## 关键技术(逃离 Minecraft 方块感)
56
+
57
+ - **Bevel 倒角修改器**给 tile/道具圆角 → 立刻脱离立方体棱角(圆润 low-poly 关键一招)。
58
+ - **BMesh `from_pydata`** 从顶点/面数据建网格;`bmesh.ops` 做 extrude/subdivide/translate。改完 `bm.to_mesh()` + `bm.free()`。
59
+ - **修改器栈**(脚本化):Bevel(圆角) / Solidify(厚度) / Subdivision(平滑,省用) / Array(阵列) / Mirror(对称)。
60
+ - **fBm/Perlin 噪声**(`mathutils/noise` 或 `noise` 库)做地形起伏;**L-system 思路**做树。
61
+ - **glTF 2.0 二进制导出** `bpy.ops.export_scene.gltf(...)`;**Decimate** 生成 LOD 层级(LOD1=50%、LOD2=75%、LOD3=90% 缩减)。
62
+ - **Flat shading** 切换可出风格化块面(是「风格化」不是「像素化」)。
63
+
64
+ ## 缝消除 & Blender 5.x 踩坑(实战沉淀 · 高复用)
65
+
66
+ ### 地面/地形必须「合并连续网格」,不能用「一堆独立方块各自做顶点色」
67
+ - **反模式(踩过)**:每个格子一个独立 cube mesh,各自 `bevel` + 各自顶点色向邻居渐变。两根打脸:① 每格独立 mesh → 倒角后每格成独立圆枕,枕间露沟槽 → **网格缝更明显**;② 草块贴沙边染向沙、沙块贴草边染向草,两边同染 → 1 格宽沙环被草色吃掉、水边糊成 muddy 窄带。
68
+ - **正确架构(对标动森/朋友生活/GoGoTown)**:地面 = **一整张连续网格(heightfield)**,不是一堆独立方块。
69
+ - 平地区域是一张连续平面 → **根本没有网格缝**。
70
+ - 顶点色 splat 混合**只在边界带**做、块中心保持纯色(每格 SUB×SUB 子顶点,中心顶点纯色)→ 沙不被吃、水边干净。
71
+ - 高度差处才生**崖壁/岸壁**(earth 材质,double-sided 免法线朝向烦恼)。
72
+ - 倒角只对**整张合并 mesh** 做且**限角** → 只磨圆崖壁/外缘,平顶 0° 绝不倒角 → 平顶无缝、圆润感留在该在处。
73
+ - 同高邻居**共享顶点**(字典 key 复用)→ 顶面对接无缝;只有临不同地形类型(非水)的邻居才 smoothstep 边界带渐变。水作为独立平面,陆地颜色**绝不向水晕染**。
74
+
75
+ ### Blender 5.x 三个必记坑
76
+ 1. **Bevel 用 `obj.modifiers.new(name="Bevel", type='BEVEL')` 拿回引用**,不用 `bpy.ops.object.modifier_add(type='BEVEL')` 后按名 `obj.modifiers["Bevel"]` 取(5.x 报 `key "Bevel" not found`)。
77
+ 2. **Bevel 必须 `limit_method='ANGLE'` + `angle_limit`(如 40°)**:默认 `NONE` 会**把所有边(含平顶内部网格线)全倒角** → 平顶重引缝。限角后只磨圆崖壁/外缘(90°),平顶 0° 不动。
78
+ 3. **5.x 材质/导出**:① 新材质 `node_tree.nodes.clear()` 后显建 Output+BSDF(无自动命名);② `ColorRamp` element `.color` 需 RGBA 四元组;③ 5.x Principled BSDF **无 `Transmission`** 输入 → 透明水面只调 `blend_method='BLEND'` + Alpha;④ `export_scene.gltf` **无 `export_colors` 参数**(5.x 已移除)→ 直接 `export_format='GLB', export_materials='EXPORT'`。
79
+ 4. **顶点色连续网格优于逐立方体**:连续网格上 splat 顶点色才能跨边界无缝衔接;逐立方体各自顶点色无法跨 mesh 衔接(同反模式①)。
80
+ 5. `look`/`get_viewport_screenshot` 在部分环境回传被过滤("model does not support images")→ **AI 无法自检视觉**,靠客观 DIAG/PROOF(面法线分布、坐标、渐变顶点数)佐证 + 用户眼睛终审。
81
+ 6. **`transform_apply(location=True)` 会把 `obj.location` 重置为 0**(位置已烘进顶点数据)。凡在 apply 之后用 `obj.location.x/y` 判定方向(如"哪条竖直棱属于哪一侧")都会退化:坐标全相对 0 → 符号全为负 → **整块所有竖直棱被误选**。**修复**:方向判定改用网格顶点真实包围盒中心 `ocx=0.5*(min+max x)`, `ocy=0.5*(min+max y)`。强烈建议:倒角/方向相关逻辑一律用顶点包围盒中心,不碰 `obj.location`。
82
+
83
+ ### 选择性倒角实现(实测坑)
84
+ - `MeshEdge.bevel_weight` 属性在 5.x **已移除**;改成网格属性 `bevel_weight_edge`,但 Bevel 修改器 `limit_method='WEIGHT'` 实测 `slant=0` 未生效(flaky)。
85
+ - **最稳做法**:用 `bmesh.ops.bevel(bm, geom=[选中的竖直棱], affect='EDGES', offset=BEVEL_W, segments=2, profile=0.5, clamp_overlap=True)` 直接对选中棱倒角 → 版本无关、确定性强。倒角后 `bm.to_mesh(mesh)` 回写,按法线重排材质,顶点色在倒角后上色(含新增顶点防黑边)。
86
+ - 判竖直棱:两顶点 `abs(x1-x2)<eps and abs(y1-y2)<eps`(忽略 z 差)→ 竖直;其 `xs=sign(x-中心x)`、`ys=sign(y-中心y)` 决定属于哪侧。
87
+
88
+ ### 导出坑(已修)
89
+ - **Blender 5.2 glTF 色导出 bug**:顶点色→白色 `COLOR_0`(ubyte)+畸形 `COLOR_1`(ushort);纯色材质→无 `pbrMetallicRoughness`。**对策:GLB 只出几何,颜色全在下游引擎(Godot/Maya/Unity)侧套** `StandardMaterial3D` / PBR。不要在 Blender 侧烤色。
90
+ - bmesh 跨会话 bevel 报 `BMEdge has been removed` → 选中+bevel+to_mesh+free 必须在**单 bmesh 会话内**完成。
91
+ - 过渡名无拓扑后缀互相覆盖 → 加 `_inland/_side/_corner` 后缀区分。
92
+
93
+ ## 连接与工具(mcp-for-blender)
94
+
95
+ - MCP 配置:`~/.workbuddy/mcp.json` 的 `blender` 节点(`command: uvx blender-mcp`, `BLENDER_PORT=9876`)。
96
+ - 插件:`~/Library/Application Support/Blender/5.2/scripts/addons/blender_mcp_addon.py`(Blender 内 Edit→Preferences→Add-ons 启用 "Blender MCP",N 面板 BlenderMCP → Start MCP Server)。
97
+ - **可复用 helper**:`/Users/jojo/AI_Projects/Seele/tools/blender_mcp_exec.py` — 读 .py 脚本或 stdin,经 `uvx blender-mcp` 桥在 Blender 内执行并返回输出:`python3 blender_mcp_exec.py script.py`(需 Blender 已开 + 插件服务端已启动)。
98
+ - 视觉自检:`look`(rendered 通道,返回场景渲图)替代旧 `get_viewport_screenshot`。
99
+
100
+ ## CC0 素材直拉(mcp-for-blender 内集成)
101
+
102
+ - **Poly Pizza**:~10,600 免费 low-poly CC0 模型,适合风格化游戏;让 agent 搜索+过滤+直接导入。
103
+ - **Poly Haven**:HDRI 环境光照 + PBR 纹理;适合离开 Blender 配光照/材质。
104
+ - **Hunyuan3D / Tripo**(经 `generate_3d`,需 API key):文本/图像生成带贴图 3D 模型,用于素材库没有的独有道具,再进 Blender 精修。
105
+ - 策略:AI 生成与 CC0 拉取混合——先拉 CC0 基线对齐视觉语言,再参数化自研专属模块。CC-BY 须署名登记来源(资产红线见 `blender-sop` §3)。
106
+
107
+ ## 迭代完善纪律
108
+
109
+ 本技能 v1.0.0 为通用化初版,使用中逐步修订:
110
+ - 每次建模发现的新技法/坑/好用的 Spec 模板 → 回写本 SKILL.md。
111
+ - 新资产类别(树/屋/角色)沉淀为可复用 bpy 函数片段,附在本技能或独立参考文件。
112
+ - 用户美术方向偏好(配色、轮廓、圆角强度)→ 记入 Spec 表默认模板。
113
+ - 不擅自推翻 `blender-sop` 级红线;涉及决策变更先与主理人(艾芙琳)对齐。
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: blender-env
3
+ description: "Blender 环境体检与排障:找不到可执行文件、版本不对、GPU/METAL、EEVEE 黑图、输出目录无权限、bpy 脚本报错、MCP 插件没连上。当 doctor 失败或用户说 Blender 跑不起来时使用。"
4
+ whenToUse: "Blender 找不到 / 版本 / METAL / GPU / 黑图 / 权限 / bpy 报错 / 插件连接 时"
5
+ license: MIT
6
+ ---
7
+
8
+ # Blender 环境体检
9
+
10
+ > 先 doctor,再怀疑脚本。多数“脚本有问题”最后是路径、版本或 GPU 后端。
11
+
12
+ ## 1. 体检顺序
13
+
14
+ | 步 | 检查 | 命令 | 失败含义 |
15
+ |---|---|---|---|
16
+ | 1 | 可执行文件 | `python3 scripts/blender_client.py doctor` | 没装 / 路径不对 |
17
+ | 2 | 版本 | 同上,看 `Blender 5.x` | 4.x 的引擎标识不同 |
18
+ | 3 | GPU 后端 | 同上,看 `METAL` | 回落到软件渲染,极慢 |
19
+ | 4 | 引擎列表 | 同上,应含 `BLENDER_EEVEE` | 版本或构建异常 |
20
+ | 5 | 冒烟 | `... smoke --res 256` | 能出图 = 链路通 |
21
+
22
+ ## 2. 常见故障
23
+
24
+ ### 找不到 Blender
25
+
26
+ - macOS 默认在 `/Applications/Blender.app/Contents/MacOS/Blender`;
27
+ - 其他位置:`export BLENDER_BIN=/path/to/Blender`,或每次 `--blender /path/to/Blender`;
28
+ - 不要指望 `blender` 在 PATH 里——App 安装通常不会建符号链接。
29
+
30
+ ### 版本不对(4.x vs 5.x)
31
+
32
+ - 5.x 的 EEVEE 标识是 `BLENDER_EEVEE`(4.2–4.x 曾用 `BLENDER_EEVEE_NEXT`);
33
+ - 部分 API 在 5.x 变更(Bevel、材质节点、`bevel_weight` 移除),见 `blender-5x-pitfalls`;
34
+ - 混用版本时,先统一到 5.2 LTS 再排查脚本。
35
+
36
+ ### 渲染黑图 / 全黑
37
+
38
+ - headless 下 EEVEE 依赖 GPU;先确认 `doctor` 的 GPU 后端是 `METAL`;
39
+ - 没有相机 → 黑图;没有灯光 → 暗图(不是黑);
40
+ - 世界背景为纯黑且没有灯光时,默认立方体也会接近黑;
41
+ - 先在 `smoke` 上验证,再怀疑场景。
42
+
43
+ ### 输出目录无权限
44
+
45
+ - 错误形如 `Permission denied` / `Operation not permitted` 写 PNG;
46
+ - 用 `--outdir` / `--out` 指到可写目录(工作区内最稳);
47
+ - 受限进程启动的 Blender 也受同样限制——必要时用可写目录重启。
48
+
49
+ ### bpy 脚本报错
50
+
51
+ - 先看完整 traceback:客户端失败时会打印最后 25 行;
52
+ - 常见:API 名称在 5.x 改名、上下文对象为空(headless 没有 UI 上下文)、`bpy.ops` 需要正确的 active object;
53
+ - headless 下避免依赖 `screen`/`area` 的操作。
54
+
55
+ ### MCP 连不上(交互模式)
56
+
57
+ - 需要 Blender 正在运行 + 装好 addon + `uvx blender-mcp`;
58
+ - headless 不需要 MCP;只有“实时操控打开着的 Blender”才需要;
59
+ - 详见 `blender-mcp-bridge`。
60
+
61
+ ## 3. 体检输出怎么读
62
+
63
+ ```
64
+ ✅ Blender: Blender 5.2.1 LTS
65
+ 路径: /Applications/Blender.app/Contents/MacOS/Blender
66
+ GPU 后端: METAL
67
+ 引擎: BLENDER_EEVEE
68
+ ```
69
+
70
+ - 版本不是 5.x → 先对齐版本或接受差异;
71
+ - GPU 不是 METAL → 检查是否被 `--factory-startup` 之外的配置改过;
72
+ - 引擎列表缺 EEVEE → 构建异常,重装。
73
+
74
+ ## 4. 边界
75
+
76
+ - 不代装 Blender、不改系统级配置;
77
+ - 不承诺渲染耗时——同一场景在不同 GPU 上差异很大;
78
+ - 环境修不好时,明确告诉用户卡在哪一步,而不是继续调脚本。
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: blender-mcp-bridge
3
+ description: "把 mcp-for-blender 接到 DSH:uvx 启动、Blender 5.2 插件、连接前提、真实工具名对齐、连通性验证与按需加载。用于需要实时操控 Blender 的交互场景。"
4
+ whenToUse: "Blender MCP / 实时操控 / 交互式 / uvx / 插件 / 连接失败 时"
5
+ license: MIT
6
+ ---
7
+
8
+
9
+ # Blender MCP 接线(blender-mcp-bridge)
10
+
11
+ > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
+
13
+
14
+ 让团队能**在运行中的 Blender 5.2 内**被智能体驱动。本机操控层是增强版 **mcp-for-blender**(经 `uvx blender-mcp` 安装,工具命名空间 `mcp__blender__*`),**不是**社区基础版 `blender-mcp`(ahujasid)。
15
+
16
+ ## 配置现状(本机已就绪)
17
+
18
+ - **MCP 配置**:`~/.workbuddy/mcp.json` 的 `blender` 节点:`command: uvx blender-mcp`,`BLENDER_PORT=9876`,`disabled:false`。
19
+ - **本地插件**:`~/Library/Application Support/Blender/5.2/scripts/addons/blender_mcp_addon.py`(175KB,2026-09-14)。
20
+ - **连接前提**:Blender 必须**开着**且插件服务端**已启动**(N 面板 → BlenderMCP → Start MCP Server)。当前没开 → 实测 `get_addon_status` 返回「Could not connect to Blender」。这与 Godot 当初"误判无 MCP"同一剧本——操控层存在,只是需用户先开软件。
21
+ - **没有** Godot 那种编辑器自动拉起;Blender MCP 需用户在 Blender 内手动点启动(或后续研究能否 CLI 拉起 + 触发服务端)。
22
+
23
+ ## 连接证明(Phase 0.5 闸门)
24
+
25
+ - 先 `get_addon_status` 确认插件/版本匹配、库与生成器开关;返回已连才算"本机 Blender 已接入"。
26
+ - 或 `execute_blender_code` 跑 `import bpy; print(bpy.app.version)` 能回显版本即证明通道通。
27
+ - **无连接证明前,任何成员不得声称能操控 Blender**——只能输出纯设计/文档产出并显式告知阻塞。
28
+
29
+ ## 真实工具矩阵(对齐 mcp-for-blender,别照基础版写旧名)
30
+
31
+ | 工具 | 能力 | 团队用途 |
32
+ |---|---|---|
33
+ | `get_addon_status` | 版本/插件匹配/库与生成器开关/telemetry | **环境审计入口** |
34
+ | `get_scene_info` | 场景事实:对象数/位置/尺寸/拓扑健康/权重 | 读回场景树 |
35
+ | `execute_blender_code` | 在用户活 Blender 跑 bpy/bmesh/mathutils | **逃逸舱**:一切结构化工具够不到的都写 Python |
36
+ | `look` | 渲图看场景:viewport/camera/多角度/帧序列,solid/material/rendered/wireframe/xray | **视觉反馈通道**(替代旧 `get_viewport_screenshot`,更强) |
37
+ | `search_assets` | 搜 polyhaven(CC0)/sketchfab/Polypizza(CC0/CC-BY) | CC0 资产直拉 |
38
+ | `generate_3d` | 文本/图像→带贴图 3D 模型,tripo/hunyuan3d/hyper3d,Premium 档 | AI 造模(独有道具),再进 Blender 精修 |
39
+ | (导出/导入) | glTF/GLB/FBX/OBJ/STL/USD/ABC/PLY | 资产出 Blender → 引擎/AR/打印 |
40
+
41
+ > ⚠️ **与旧基础版 blender-mcp 的工具名差异**(建脚本/技能时务必对齐本表):
42
+ > 旧 `get_viewport_screenshot` → 现 `look`;旧 `get_polyhaven_assets` → 现 `search_assets`;旧 `generate_hyper3d_model` → 现 `generate_3d`。直接照搬旧名会调不到工具。
43
+
44
+ ## 每成员工具配方
45
+
46
+ | 成员 | 常用工具 |
47
+ |---|---|
48
+ | blender-tech-mcp | get_addon_status / execute_blender_code / 导出全系 / search_assets / generate_3d |
49
+ | blender-modeling | execute_blender_code / get_scene_info / look / generate_3d / export glTF |
50
+ | blender-shading | execute_blender_code / get_scene_info / look / search_assets |
51
+ | blender-rig-anim | execute_blender_code / get_scene_info / look |
52
+ | blender-light-render | execute_blender_code / get_scene_info / look |
53
+ | blender-sim-vfx | execute_blender_code / get_scene_info / look |
54
+
55
+ ## 按需加载(对抗小模型幻觉)
56
+
57
+ - 只亮少量核心工具(get_addon_status / get_scene_info / execute_blender_code / look),重工具经 `search_assets` / `generate_3d` 调用。
58
+ - 范本来例:StraySpark Blender MCP Server v4.0(556 工具/39 类,只开 22 个开门工具,其余经 search 按需加载,上下文压到 9.5K token)。这套技能不照搬其 556 工具,但**同一哲学**:工具多 ≠ 全塞上下文。
59
+ - 复用 Godot 团教训:seele-godot 131 工具全量注入小模型 → 幻觉。Blender 团避免。
60
+
61
+ ## 可复用 helper
62
+
63
+ `/Users/jojo/AI_Projects/Seele/tools/blender_mcp_exec.py` — 读 .py 或 stdin,经 `uvx blender-mcp` 桥在 Blender 内执行并返回输出:`python3 blender_mcp_exec.py script.py`。可作团队脚本资源(注意:需 Blender 已开 + 插件服务端已启动)。
64
+
65
+ ## 红线
66
+ - 默认本地优先(127.0.0.1);云端造模(`generate_3d` / fal.ai)经 `uvx` 运行时,须显式告知用户数据出站。
67
+ - 无连接证明不得声称已操控。
68
+ - 截图/`look` 仅用于自动化自检,不替代人的视觉终裁。
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: blender-modeling-cookbook
3
+ description: "Blender 建模技法库:参数化 bpy 构建、BMesh 操作、修改器栈、拓扑规则、UV 展开,以及让 low-poly 资产圆润的选择性 Bevel 配方。"
4
+ whenToUse: "Blender 建模 / bpy / BMesh / 修改器 / 拓扑 / UV / Bevel 时"
5
+ license: MIT
6
+ ---
7
+
8
+
9
+ # 建模技法库(blender-modeling-cookbook)
10
+
11
+ > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
+
13
+
14
+ **沉淀实战建模技法,供 `blender-modeling` 与全员复用。** 锚定 Blender 5.2 LTS;语法不假设 4.x。
15
+
16
+ ## §1 参数化 bpy 构建范式
17
+
18
+ - **代码即资产**:优先 bpy 参数化脚本(可版本化、可 diff、可复现)。模板:
19
+ ```python
20
+ import bpy, bmesh, mathutils
21
+ mesh = bpy.data.meshes.new("prop"); obj = bpy.data.objects.new("prop", mesh)
22
+ bpy.context.collection.objects.link(obj)
23
+ bm = bmesh.new(); # ... from_pydata / ops ... ; bm.to_mesh(mesh); bm.free()
24
+ ```
25
+ - **LL3M 自批判循环**:生成 → `execute_blender_code` 执行 → `get_scene_info`/`look` 读回 → 修正。结构化操作优先落脚本而非依赖不可控的结构化工具。
26
+
27
+ ## §2 BMesh 操作
28
+
29
+ - 建网格:`bm.from_mesh(mesh)` 或 `bm.from_pydata(verts, edges, faces)`。
30
+ - 常用 ops:`bmesh.ops.create_cube` / `extrude_face_region` / `subdivide_edges` / `translate` / `bevel`(直接对选中几何)。
31
+ - 倒角(最稳):`bmesh.ops.bevel(bm, geom=[选中棱], affect='EDGES', offset=W, segments=2, profile=0.5, clamp_overlap=True)` —— 选中+bevel+to_mesh+free 必须在**单 bmesh 会话内**。
32
+ - 判竖直棱:两顶点 `abs(x1-x2)<eps and abs(y1-y2)<eps`;方向用**顶点包围盒中心**(`0.5*(min+max)`),**不碰 `obj.location`**(`transform_apply(location=True)` 会把它归零导致符号全部翻转)。
33
+
34
+ ## §3 修改器栈(脚本化)
35
+
36
+ | 修改器 | 用途 |
37
+ |---|---|
38
+ | Bevel | 圆角,逃离方块感(限角 `ANGLE` + `angle_limit`) |
39
+ | Solidify | 厚度(墙面/叶片) |
40
+ | Subdivision | 平滑(省用,重) |
41
+ | Array | 阵列(重复元素) |
42
+ | Mirror | 对称(角色/道具) |
43
+ | WeightedNormal | 法线平滑(低模关键) |
44
+
45
+ - 加 Bevel 务必 `obj.modifiers.new(name="Bevel", type='BEVEL')` 拿引用,不用 `bpy.ops.object.modifier_add` 后按名取(5.x `key "Bevel" not found`)。
46
+ - Bevel `limit_method='ANGLE'` + `angle_limit`(如 40°):平顶内部网格线(0°)不倒角,只磨圆崖壁/外缘(90°)。
47
+
48
+ ## §4 拓扑规则
49
+
50
+ - **连续网格优于逐立方体**:地面/地形用一整张 heightfield,顶点色 splat 只在边界带、块中心纯色 → 无缝、色不糊。逐立方体各自顶点色无法跨 mesh 衔接(缝 + 色糊反模式)。
51
+ - **低模代理**:quad 优先、拓扑干净、三角预算写死(tile ≤ 200、树 ≤ 1500)。
52
+ - **重拓扑/UV/绑定必须人审**(StraySpark 硬限制 #1)。
53
+
54
+ ## §5 UV 展开
55
+
56
+ - 智能 UV 投影 / 展 UV(Unwrap)/ 打包(Pack Islands);命名规范,避免重叠。
57
+ - 接缝(seam)沿结构暗线;对称模型用 Mirror 同步 UV。
58
+
59
+ ## §6 LOD 与导出
60
+
61
+ - Decimate 生成 LOD(LOD1=50% / LOD2=75% / LOD3=90% 缩减)。
62
+ - glTF/GLB 只出几何(5.2 丢色),颜色下游引擎套(见 `blender-5x-pitfalls` / `blender-sop` §4)。
63
+
64
+ ## §7 复用资产
65
+
66
+ - `generate_3d`(tripo/hunyuan3d/hyper3d)云端造模独有道具 → 进 Blender 精修。
67
+ - `search_assets` 直拉 Poly Pizza CC0 low-poly 作基线,对齐视觉语言后参数化自研。
68
+ - helper:`/Users/jojo/AI_Projects/Seele/tools/blender_mcp_exec.py` 本地执行 bpy 脚本。
@@ -0,0 +1,64 @@
1
+ ---
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 / 导出规范 时"
5
+ license: MIT
6
+ ---
7
+
8
+
9
+ # Blender 专业标准(blender-sop)
10
+
11
+ > DSH 执行层(headless 跑 Blender、渲染、导出):见 `blender-studio` 技能。
12
+
13
+
14
+ **所有管线判断以此为准。** 本技能是 Blender 工作室的"大脑",版本/渲染器/资产/导出/协作纪律的单一真相源。
15
+
16
+ ## §1 版本基线(务必写准,别犯 Godot 的版本错)
17
+
18
+ - **最新稳定**:Blender **5.2 LTS**(2026-09-15 发布,支持到 2028-07)。本机已装 **5.2.1 LTS**(`/Applications/Blender.app`)。
19
+ - **次选长期支持**:4.5 LTS(2025-07,支持到 2027-07)。4.2 LTS 已于 2026-07 EOL。
20
+ - **结论**:团队默认目标 **5.2 LTS**;兼容下限 4.5 LTS。渲染器 **Cycles(写实)/ Eevee(实时)** 双轨,5.x Eevee 已重写(EEVEE-Next),**不再假设 4.x 旧 Eevee 行为**。
21
+ - **红线**:脚本/修改器语法必须锚定 5.x;不假设 4.x。
22
+
23
+ ## §2 渲染器抉择
24
+
25
+ | 场景 | 选 | 理由 |
26
+ |---|---|---|
27
+ | 写实 / 离线 / 产品图 | **Cycles** | 路径追踪,物理准确 |
28
+ | 实时 / 预览 / 交互 / 风格化 | **Eevee(EEVEE-Next)** | 5.x 重写,快但近似 |
29
+ | 动画序列批量出 | Cycles(降采样+降噪)或 Eevee | 看预算 |
30
+
31
+ ⚠️ 不假设 4.x Eevee 行为;5.x 材质/灯光节点有差异。
32
+
33
+ ## §3 资产红线(商用前置)
34
+
35
+ - 商用前必须 **CC0 或已授权**,禁未授权素材进包。
36
+ - `search_assets` 直拉的 **Poly Haven(CC0)/ Poly Pizza(CC0/CC-BY)/ Sketchfab(须看授权)**——CC-BY 须署名,登记来源。
37
+ - 用户自有/自建模产可商用;AI 生成(`generate_3d`)须确认生成器授权(tripo/hunyuan3d/hyper3d 商用条款)。
38
+
39
+ ## §4 导出纪律(glTF/USD/FBX)
40
+
41
+ - **glTF/GLB**:游戏/AR 默认。本环境 **5.2 glTF 导出器丢色**(顶点色→白色 `COLOR_0`+畸形 `COLOR_1`;纯色材质→无 `pbrMetallicRoughness`)→ **GLB 只出几何,颜色在下游引擎侧套**(Godot/Maya/Unity 建材质)。
42
+ - **FBX**:Maya/Unity 互通。
43
+ - **USD**:Blender→Unreal 一键管线(多 agent 跨软件)。
44
+ - **Decimate** 生成 LOD(LOD1=50% / LOD2=75% / LOD3=90% 缩减)。
45
+ - 导出参数(5.x):`export_format='GLB'`、`export_materials='EXPORT'`(**无 `export_colors` 参数**,5.x 已移除)。
46
+
47
+ ## §5 Agent+MCP 协作三硬限制(StraySpark)
48
+
49
+ 1. **创意拓扑不是 agent 的活**:概念/大形/风格/轮廓决策由**人**做;agent 只接手可参数化、可验证子任务(blockout / 低模代理)。手动重拓扑/UV/绑定/动画必须人审。
50
+ 2. **重操作超时 60–120s**:细分/重拓扑/烘焙/模拟 bake 会超时——须拆小步、降分辨率、或交人手动在 Blender 内跑。
51
+ 3. **agent 无视觉反馈**:agent 看不到渲染结果,靠 `look`/`get_scene_info` 间接判断——**所有"看起来对不对"的终裁必须落回人**(agent 不自认已验证)。
52
+
53
+ ## §6 Blender-MCP 协作铁律
54
+
55
+ - **连接前提**:Blender 必须开着且插件服务端已启动(N 面板 → BlenderMCP → Start MCP Server)。无连接证明(如 `get_addon_status` 已连)前,任何成员不得声称能操控 Blender。
56
+ - **执行后必 `look` 自检**:`look` 是 agent 唯一视觉通道,是硬限制 #3 的补偿机制。
57
+ - **代码即资产**:结构化操作优先落 bpy 脚本(`execute_blender_code`),可版本化、可 diff、可复现;LL3M 自批判循环 = 执行 → 读回 → 修正。
58
+ - **工具按需加载**:只亮少量核心工具(get_addon_status / get_scene_info / execute_blender_code / look),重工具经 `search_assets`/`generate_3d` 调用,别全量塞(复用 Godot 团"131 工具全量注入→幻觉"教训)。
59
+ - **造模上云、精修在本地**:`generate_3d`(tripo/hunyuan3d/hyper3d)云端造模绕开本地重算超时(硬限制 #2);精修留本地 bpy。
60
+ - **隐私**:默认本地优先(127.0.0.1);云端造模经 `uvx` 运行时,须显式告知用户数据出站。
61
+
62
+ ## §7 修改与同步纪律
63
+
64
+ 本技能为团队标准基线。每次建模/渲染发现的新技法/坑/规范 → 回写本 SKILL.md 对应章节 + MEMORY.md(项目级)。不擅自推翻 §1–§6 级红线;涉及决策变更先与主理人(艾芙琳)对齐。
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: blender-studio
3
+ description: "Blender 在 DSH 里的总入口:环境体检、headless 跑 bpy 脚本、渲染 .blend、导出 glTF,以及建模/踩坑/资产管线/MCP 的路由。当用户要建模、渲染、做游戏资产、批量生成或接 Blender 时使用。"
4
+ whenToUse: "Blender / bpy / 渲染 / render / glTF / GLB / 建模 / 资产 / headless 时"
5
+ license: MIT
6
+ ---
7
+
8
+ # Blender × DSH
9
+
10
+ > 本技能是执行层:怎么在 DSH 里用 headless Blender 跑脚本、渲染、导出。
11
+ > 领域知识(SOP、建模、踩坑、资产管线)在另外几个技能里,见第 5 节路由。
12
+
13
+ ## 0. 先决条件
14
+
15
+ - Blender **5.2 LTS**(本包按 5.2 校准;4.x 的引擎标识与 API 有差异);
16
+ - macOS 默认路径 `/Applications/Blender.app/Contents/MacOS/Blender`;
17
+ - 用 `BLENDER_BIN` 或 `--blender` 指定其他路径;
18
+ - 客户端零依赖(Python 3.8+),不需要在系统 Python 里装 bpy。
19
+
20
+ ## 1. 30 秒体检
21
+
22
+ ```bash
23
+ python3 scripts/blender_client.py doctor
24
+ ```
25
+
26
+ 输出:版本、可执行文件路径、GPU 后端(macOS 应为 `METAL`)、可用渲染引擎。
27
+ 连不上/找不到时先看 `blender-env`,不要改脚本。
28
+
29
+ ## 2. 冒烟渲染(证明链路是通的)
30
+
31
+ ```bash
32
+ python3 scripts/blender_client.py smoke --outdir outputs/blender/smoke --res 256
33
+ ```
34
+
35
+ 会新建默认场景、用 EEVEE 渲染一张 256×256 PNG。3–5 秒内出图即正常。
36
+
37
+ ## 3. 跑 bpy 脚本(主要工作方式)
38
+
39
+ ```bash
40
+ python3 scripts/blender_client.py run \
41
+ --script skills/blender-studio/examples/make_tiles.py \
42
+ --outdir outputs/blender/tiles
43
+ ```
44
+
45
+ - 脚本通过环境变量 `DSH_BLENDER_OUTDIR` 拿到输出目录;
46
+ - 需要从已存工程开始时加 `--blend path/to/scene.blend`;
47
+ - 额外参数:`--arg key=value`(追加到 `--` 之后,脚本用 `sys.argv` 读)。
48
+
49
+ 示例 `examples/make_tiles.py` 演示了完整闭环:参数化建模 → 选择性 Bevel → 相机灯光 → EEVEE 渲染 → 保存 `.blend` → 导出 `.glb`。
50
+
51
+ ## 4. 渲染已有工程 / 导出
52
+
53
+ ```bash
54
+ # 渲染单帧
55
+ python3 scripts/blender_client.py render \
56
+ --blend outputs/blender/tiles/scene.blend \
57
+ --out outputs/blender/tiles/final.png \
58
+ --samples 64 --res 1024
59
+
60
+ # 更复杂的导出(glTF / USD / FBX)直接在脚本里做,不要在命令行拼
61
+ python3 scripts/blender_client.py run --script my_export.py --blend scene.blend
62
+ ```
63
+
64
+ 输出统一放 `outputs/blender/<主题>/`,文件名带版本或日期;生成后用 `present` 把图片给用户看。
65
+
66
+ ## 5. 领域路由
67
+
68
+ | 用户问题 | 调用技能 |
69
+ |---|---|
70
+ | 找不到 Blender / 版本不对 / 渲染报错 | `blender-env` |
71
+ | 渲染器怎么选、导出规范、CC0 红线 | `blender-sop` |
72
+ | 建模、Bevel、修改器、拓扑、UV | `blender-modeling-cookbook` |
73
+ | 5.x 的 API 变化与已知坑 | `blender-5x-pitfalls` |
74
+ | 程序化资产生成、批量、LOD、glTF | `blender-asset-pipeline` |
75
+ | 要实时操控打开着的 Blender | `blender-mcp-bridge` |
76
+
77
+ ## 6. 诚实边界
78
+
79
+ - **headless 只适合确定性任务**:建模、材质、渲染、导出、批量。雕刻、手动绑定、视觉微调这类交互工作,要么用 MCP 接活着的 Blender,要么交给人。
80
+ - 不代装 Blender、不代下资产(CC0 规则见 `blender-sop`)。
81
+ - 渲染质量与速度取决于本机 GPU;同一场景的耗时不能直接跨机器比较。
82
+ - 所有操作只在本机执行,不经过云端。
@@ -0,0 +1,98 @@
1
+ """
2
+ 示例:参数化 low-poly 地砖 + 选择性 Bevel + 渲染 + glTF 导出。
3
+
4
+ 在 DSH 里跑:
5
+ python3 skills/blender-studio/scripts/blender_client.py run \
6
+ --script skills/blender-studio/examples/make_tiles.py \
7
+ --outdir outputs/blender/tiles
8
+
9
+ 输出(默认 <outdir>):
10
+ scene.blend 可继续编辑的场景
11
+ tiles.png 512×512 EEVEE 渲染
12
+ tiles.glb glTF 导出(游戏资产)
13
+ """
14
+ import os
15
+ import math
16
+
17
+ import bpy
18
+ from mathutils import Vector
19
+
20
+ OUTDIR = os.environ.get("DSH_BLENDER_OUTDIR", "outputs/blender/tiles")
21
+ os.makedirs(OUTDIR, exist_ok=True)
22
+
23
+ # 1. 空场景
24
+ bpy.ops.wm.read_factory_settings(use_empty=True)
25
+ scene = bpy.context.scene
26
+
27
+ # 2. 参数:3×3 地砖,间隙与厚度
28
+ GRID = 3
29
+ SIZE = 1.6
30
+ GAP = 0.08
31
+ HEIGHT = 0.18
32
+ BEVEL = 0.045
33
+
34
+ mat = bpy.data.materials.new("Tile")
35
+ mat.use_nodes = True
36
+ bsdf = mat.node_tree.nodes.get("Principled BSDF")
37
+ bsdf.inputs["Base Color"].default_value = (0.62, 0.66, 0.72, 1.0)
38
+ bsdf.inputs["Roughness"].default_value = 0.55
39
+
40
+ for ix in range(GRID):
41
+ for iy in range(GRID):
42
+ bpy.ops.mesh.primitive_cube_add(size=1)
43
+ obj = bpy.context.active_object
44
+ obj.name = f"tile_{ix}_{iy}"
45
+ obj.scale = (SIZE / 2, SIZE / 2, HEIGHT / 2)
46
+ obj.location = (
47
+ (ix - (GRID - 1) / 2) * (SIZE + GAP),
48
+ (iy - (GRID - 1) / 2) * (SIZE + GAP),
49
+ HEIGHT / 2,
50
+ )
51
+ bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
52
+ obj.data.materials.append(mat)
53
+
54
+ bevel = obj.modifiers.new("Bevel", "BEVEL")
55
+ bevel.width = BEVEL
56
+ bevel.segments = 3
57
+ bevel.limit_method = "ANGLE"
58
+
59
+ # 3. 相机与灯光
60
+ bpy.ops.object.camera_add(location=(4.2, -4.8, 4.0))
61
+ cam = bpy.context.active_object
62
+ cam.rotation_euler = (math.radians(52), 0, math.radians(42))
63
+ scene.camera = cam
64
+
65
+ bpy.ops.object.light_add(type="SUN", location=(3, -2, 6))
66
+ sun = bpy.context.active_object
67
+ sun.data.energy = 3.2
68
+ sun.rotation_euler = (math.radians(35), 0, math.radians(30))
69
+
70
+ # 4. 世界背景
71
+ world = bpy.data.worlds.new("World")
72
+ scene.world = world
73
+ world.use_nodes = True
74
+ world.node_tree.nodes["Background"].inputs[0].default_value = (0.05, 0.06, 0.08, 1.0)
75
+
76
+ # 5. 渲染设置(EEVEE + 512)
77
+ scene.render.engine = "BLENDER_EEVEE"
78
+ scene.render.resolution_x = 512
79
+ scene.render.resolution_y = 512
80
+ scene.render.resolution_percentage = 100
81
+ scene.render.image_settings.file_format = "PNG"
82
+ scene.eevee.taa_render_samples = 32
83
+
84
+ # 6. 保存工程 + 渲染
85
+ blend_path = os.path.join(OUTDIR, "scene.blend")
86
+ bpy.ops.wm.save_as_mainfile(filepath=blend_path)
87
+
88
+ scene.render.filepath = os.path.join(OUTDIR, "tiles.png")
89
+ bpy.ops.render.render(write_still=True)
90
+
91
+ # 7. 导出 glTF(游戏资产)
92
+ bpy.ops.export_scene.gltf(
93
+ filepath=os.path.join(OUTDIR, "tiles.glb"),
94
+ export_format="GLB",
95
+ use_selection=False,
96
+ )
97
+
98
+ print("DSH_EXAMPLE_OK", OUTDIR)
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ blender_client.py — 零依赖 Blender headless 客户端(仅标准库)。
4
+
5
+ 用法:
6
+ python3 blender_client.py doctor [--blender PATH]
7
+ python3 blender_client.py smoke [--outdir DIR] [--res 256]
8
+ python3 blender_client.py render --blend SCENE.blend --out PNG [--engine BLENDER_EEVEE]
9
+ [--samples 32] [--res 512] [--frame 1] [--timeout 600]
10
+ python3 blender_client.py run --script SCRIPT.py [--blend SCENE.blend] [--outdir DIR]
11
+ [--timeout 600] [--arg KEY=VALUE ...]
12
+
13
+ 默认自动查找 Blender(BLENDER_BIN 环境变量 → /Applications/Blender.app → PATH)。
14
+ 退出码:0 成功;2 找不到 Blender;4 Blender 执行失败或超时。
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import time
27
+ from pathlib import Path
28
+
29
+ CANDIDATES = [
30
+ os.environ.get("BLENDER_BIN"),
31
+ "/Applications/Blender.app/Contents/MacOS/Blender",
32
+ shutil.which("blender"),
33
+ ]
34
+
35
+
36
+ def fail(message: str, code: int):
37
+ print(message, file=sys.stderr)
38
+ raise SystemExit(code)
39
+
40
+
41
+ def find_blender(explicit: str | None = None) -> str:
42
+ for candidate in ([explicit] if explicit else []) + CANDIDATES:
43
+ if candidate and Path(candidate).exists():
44
+ return candidate
45
+ fail(
46
+ "❌ 找不到 Blender。用 --blender 指定路径,或设置 BLENDER_BIN。\n"
47
+ " macOS 默认路径:/Applications/Blender.app/Contents/MacOS/Blender",
48
+ 2,
49
+ )
50
+
51
+
52
+ def run_blender(cmd: list[str], timeout: float) -> subprocess.CompletedProcess:
53
+ try:
54
+ return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
55
+ except subprocess.TimeoutExpired:
56
+ fail(f"❌ Blender 超时({timeout}s),已终止。", 4)
57
+
58
+
59
+ def tail(text: str, n: int = 25) -> str:
60
+ lines = [line for line in (text or "").splitlines() if line.strip()]
61
+ return "\n".join(lines[-n:])
62
+
63
+
64
+ def run_expr(blender: str, expr: str, timeout: float = 120) -> subprocess.CompletedProcess:
65
+ return run_blender([blender, "-b", "--factory-startup", "--python-expr", expr], timeout)
66
+
67
+
68
+ def probe(blender: str) -> dict:
69
+ expr = (
70
+ "import bpy, json;"
71
+ "print('DSH_PROBE' + json.dumps({"
72
+ "'version': bpy.app.version_string,"
73
+ "'engines': [e.identifier for e in bpy.types.RenderSettings.bl_rna.properties['engine'].enum_items],"
74
+ "'gpu': bpy.context.preferences.system.gpu_backend"
75
+ "}))"
76
+ )
77
+ result = run_expr(blender, expr, timeout=120)
78
+ for line in (result.stdout or "").splitlines():
79
+ if line.startswith("DSH_PROBE"):
80
+ return json.loads(line[len("DSH_PROBE"):])
81
+ fail("❌ 无法从 Blender 读取环境信息:\n" + tail(result.stdout + result.stderr), 4)
82
+
83
+
84
+ def version_of(blender: str) -> str:
85
+ result = run_blender([blender, "--version"], timeout=60)
86
+ first = (result.stdout or "").splitlines()[0] if result.stdout else ""
87
+ return first.strip() or "?"
88
+
89
+
90
+ def cmd_doctor(args) -> int:
91
+ blender = find_blender(args.blender)
92
+ info = probe(blender)
93
+ print("✅ Blender:", version_of(blender))
94
+ print(" 路径:", blender)
95
+ print(" GPU 后端:", info.get("gpu", "?"))
96
+ print(" 引擎:", ", ".join(info.get("engines") or []))
97
+ return 0
98
+
99
+
100
+ def resolution(value: str) -> tuple[int, int]:
101
+ match = re.fullmatch(r"(\d+)(?:[xX](\d+))?", value.strip())
102
+ if not match:
103
+ fail(f"❌ 分辨率格式不对:{value}(例:512 或 768x512)", 3)
104
+ w = int(match.group(1))
105
+ h = int(match.group(2) or w)
106
+ return w, h
107
+
108
+
109
+ def run_python_script(blender: str, script_path: Path, blend: str | None, timeout: float, extra: list[str]) -> subprocess.CompletedProcess:
110
+ cmd = [blender, "-b"]
111
+ cmd += [blend] if blend else ["--factory-startup"]
112
+ cmd += ["--python", str(script_path)]
113
+ if extra:
114
+ cmd += ["--"] + extra
115
+ return run_blender(cmd, timeout)
116
+
117
+
118
+ def cmd_smoke(args) -> int:
119
+ blender = find_blender(args.blender)
120
+ outdir = Path(args.outdir).expanduser().resolve()
121
+ outdir.mkdir(parents=True, exist_ok=True)
122
+ out = outdir / "smoke.png"
123
+ w, h = resolution(str(args.res))
124
+ script = f"""
125
+ import bpy
126
+ scene = bpy.context.scene
127
+ scene.render.engine = 'BLENDER_EEVEE'
128
+ scene.render.resolution_x = {w}
129
+ scene.render.resolution_y = {h}
130
+ scene.render.resolution_percentage = 100
131
+ scene.render.image_settings.file_format = 'PNG'
132
+ scene.render.filepath = {str(out)!r}
133
+ bpy.ops.render.render(write_still=True)
134
+ print('DSH_RENDER_OK')
135
+ """
136
+ started = time.time()
137
+ with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tmp:
138
+ tmp.write(script)
139
+ tmp_path = Path(tmp.name)
140
+ try:
141
+ result = run_python_script(blender, tmp_path, None, args.timeout, [])
142
+ finally:
143
+ tmp_path.unlink(missing_ok=True)
144
+ if result.returncode != 0 or not out.exists():
145
+ fail("❌ 冒烟渲染失败:\n" + tail(result.stdout + result.stderr), 4)
146
+ print(f"✅ 冒烟渲染完成({time.time() - started:.1f}s)")
147
+ print(" ", out)
148
+ return 0
149
+
150
+
151
+ def cmd_render(args) -> int:
152
+ blender = find_blender(args.blender)
153
+ blend = Path(args.blend).expanduser().resolve()
154
+ if not blend.exists():
155
+ fail(f"❌ 找不到 .blend:{blend}", 3)
156
+ out = Path(args.out).expanduser().resolve()
157
+ out.parent.mkdir(parents=True, exist_ok=True)
158
+ w, h = resolution(str(args.res))
159
+ script = f"""
160
+ import bpy
161
+ scene = bpy.context.scene
162
+ scene.render.engine = {args.engine!r}
163
+ scene.render.resolution_x = {w}
164
+ scene.render.resolution_y = {h}
165
+ scene.render.resolution_percentage = 100
166
+ scene.render.image_settings.file_format = 'PNG'
167
+ scene.render.filepath = {str(out)!r}
168
+ scene.frame_set({int(args.frame)})
169
+ try:
170
+ scene.eevee.taa_render_samples = {int(args.samples)}
171
+ except Exception:
172
+ pass
173
+ bpy.ops.render.render(write_still=True)
174
+ print('DSH_RENDER_OK')
175
+ """
176
+ started = time.time()
177
+ with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tmp:
178
+ tmp.write(script)
179
+ tmp_path = Path(tmp.name)
180
+ try:
181
+ result = run_python_script(blender, tmp_path, str(blend), args.timeout, [])
182
+ finally:
183
+ tmp_path.unlink(missing_ok=True)
184
+ if result.returncode != 0 or not out.exists():
185
+ fail("❌ 渲染失败:\n" + tail(result.stdout + result.stderr), 4)
186
+ print(f"✅ 渲染完成({time.time() - started:.1f}s)")
187
+ print(" ", out)
188
+ return 0
189
+
190
+
191
+ def cmd_run(args) -> int:
192
+ blender = find_blender(args.blender)
193
+ script = Path(args.script).expanduser().resolve()
194
+ if not script.exists():
195
+ fail(f"❌ 找不到脚本:{script}", 3)
196
+ blend = str(Path(args.blend).expanduser().resolve()) if args.blend else None
197
+ if blend and not Path(blend).exists():
198
+ fail(f"❌ 找不到 .blend:{blend}", 3)
199
+ if args.outdir:
200
+ os.environ["DSH_BLENDER_OUTDIR"] = str(Path(args.outdir).expanduser().resolve())
201
+ started = time.time()
202
+ result = run_python_script(blender, script, blend, args.timeout, args.arg)
203
+ if result.returncode != 0:
204
+ fail("❌ 脚本执行失败:\n" + tail(result.stdout + result.stderr), 4)
205
+ print(f"✅ 执行完成({time.time() - started:.1f}s)")
206
+ if args.outdir:
207
+ print(" 输出目录:", Path(args.outdir).expanduser().resolve())
208
+ return 0
209
+
210
+
211
+ def main() -> int:
212
+ ap = argparse.ArgumentParser(description="Blender headless 零依赖客户端")
213
+ ap.add_argument("--blender", default=None, help="Blender 可执行文件路径")
214
+ sub = ap.add_subparsers(dest="cmd", required=True)
215
+
216
+ sub.add_parser("doctor")
217
+
218
+ p_smoke = sub.add_parser("smoke")
219
+ p_smoke.add_argument("--outdir", default="outputs/blender/smoke")
220
+ p_smoke.add_argument("--res", default="256")
221
+ p_smoke.add_argument("--timeout", type=float, default=300)
222
+
223
+ p_render = sub.add_parser("render")
224
+ p_render.add_argument("--blend", required=True)
225
+ p_render.add_argument("--out", required=True)
226
+ p_render.add_argument("--engine", default="BLENDER_EEVEE")
227
+ p_render.add_argument("--samples", type=int, default=32)
228
+ p_render.add_argument("--res", default="512")
229
+ p_render.add_argument("--frame", type=int, default=1)
230
+ p_render.add_argument("--timeout", type=float, default=900)
231
+
232
+ p_run = sub.add_parser("run")
233
+ p_run.add_argument("--script", required=True)
234
+ p_run.add_argument("--blend", default=None)
235
+ p_run.add_argument("--outdir", default=None)
236
+ p_run.add_argument("--timeout", type=float, default=900)
237
+ p_run.add_argument("--arg", action="append", default=[])
238
+
239
+ args = ap.parse_args()
240
+ handlers = {"doctor": cmd_doctor, "smoke": cmd_smoke, "render": cmd_render, "run": cmd_run}
241
+ return handlers[args.cmd](args)
242
+
243
+
244
+ if __name__ == "__main__":
245
+ sys.exit(main())