@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.
- package/README.md +7 -0
- package/package.json +2 -2
- package/skills/blender-5x-pitfalls/SKILL.md +157 -7
- package/skills/blender-asset-pipeline/SKILL.md +19 -13
- package/skills/blender-mcp-bridge/SKILL.md +24 -17
- package/skills/blender-mcp-bridge/scripts/_console.py +40 -0
- package/skills/blender-mcp-bridge/scripts/blender_mcp_exec.py +331 -0
- package/skills/blender-modeling-cookbook/SKILL.md +15 -9
- package/skills/blender-sop/SKILL.md +74 -9
- package/skills/blender-sop/scripts/check_table_consistency.py +240 -0
- package/skills/blender-sop/scripts/check_templates.py +165 -0
- package/skills/blender-sop/templates/asset_list.md +43 -0
- package/skills/blender-sop/templates/delivery_checklist.md +60 -0
- package/skills/blender-sop/templates/lighting_exposure.md +40 -0
- package/skills/blender-sop/templates/model_spec.md +40 -0
- package/skills/blender-sop/templates/naming_export.md +61 -0
- package/skills/blender-sop/templates/uv_unwrap.md +38 -0
- package/skills/blender-studio/SKILL.md +10 -1
- package/skills/blender-triage/SKILL.md +139 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""跨表咬合检查(sop 纪律 3 的机器侧判据)。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python3 scripts/check_table_consistency.py <表A.md> <表B.md> # 双表对查,有矛盾 exit 1
|
|
7
|
+
python3 scripts/check_table_consistency.py --selftest # 自测(5 真实矛盾全中 + 正常对 0 误报)
|
|
8
|
+
|
|
9
|
+
口径(七项字段取六类可机查项):
|
|
10
|
+
size 尺寸(米)/ tris 面数预算(目标+上限)/ uv UV 通道配置 / mat 材质名 /
|
|
11
|
+
fmt 导出格式(主件)/ asset 资产名
|
|
12
|
+
规则:同类字段在两份表里**都有声明**且取值集合完全不相交 → 判矛盾(每条带文件行号)。
|
|
13
|
+
范围限制(如实):自由文本抽取,单主资产/同资产多表场景适用;多资产项目按资产分表分别跑。
|
|
14
|
+
本判据的真阳性基准 = 2026-11-02 实机测试 5 处真实矛盾(见 --selftest fixtures 溯源注记)。
|
|
15
|
+
"""
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import unicodedata
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def norm(s):
|
|
24
|
+
return unicodedata.normalize("NFKC", s)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def num(tok):
|
|
28
|
+
try:
|
|
29
|
+
return str(float(tok)).rstrip("0").rstrip(".") if "." in tok else str(int(tok))
|
|
30
|
+
except ValueError:
|
|
31
|
+
return tok
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def lines_of(text):
|
|
35
|
+
return [(i + 1, ln) for i, ln in enumerate(norm(text).splitlines())]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# —— 六类字段抽取器:返回 (value, lineno) 列表 ——————————————————————————
|
|
39
|
+
|
|
40
|
+
def ex_size(rows):
|
|
41
|
+
out = []
|
|
42
|
+
ctx = re.compile(r"尺寸|外廓|大小|规格|尺度|箱体|尺寸参考")
|
|
43
|
+
tup = re.compile(r"(\d+(?:\.\d+)?)\s*[×xX\*]\s*(\d+(?:\.\d+)?)\s*[×xX\*]\s*(\d+(?:\.\d+)?)")
|
|
44
|
+
bare = re.compile(r"(\d+(?:\.\d+)?)\s*(?:m|米)\b")
|
|
45
|
+
for i, ln in rows:
|
|
46
|
+
if not ctx.search(ln):
|
|
47
|
+
continue
|
|
48
|
+
for m in tup.finditer(ln):
|
|
49
|
+
out.append(("x".join(num(g) for g in m.groups()), i))
|
|
50
|
+
for m in bare.finditer(ln):
|
|
51
|
+
out.append((num(m.group(1)), i))
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def ex_tris(rows):
|
|
56
|
+
out = []
|
|
57
|
+
ctx = re.compile(r"面数|三角面|tri|预算|上限")
|
|
58
|
+
val = re.compile(r"(?:≤|<=|目标|上限)\s*(\d+)|(\d+)\s*tri\b")
|
|
59
|
+
for i, ln in rows:
|
|
60
|
+
if not ctx.search(ln):
|
|
61
|
+
continue
|
|
62
|
+
for m in val.finditer(ln):
|
|
63
|
+
out.append((num(m.group(1) or m.group(2)), i))
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ex_uv(rows):
|
|
68
|
+
"""UV 配置声明:多通道表达式(UV0+UV1)或单通道声明(仅 UV0 / UV0 单通道)。"""
|
|
69
|
+
out = []
|
|
70
|
+
multi = re.compile(r"UV(\d+)\s*\+\s*UV(\d+)")
|
|
71
|
+
single = re.compile(r"仅\s*UV(\d+)|UV(\d+)\s*单通道")
|
|
72
|
+
for i, ln in rows:
|
|
73
|
+
for m in multi.finditer(ln):
|
|
74
|
+
out.append(("UV%s+UV%s" % (m.group(1), m.group(2)), i))
|
|
75
|
+
for m in single.finditer(ln):
|
|
76
|
+
out.append(("UV%s" % (m.group(1) or m.group(2)), i))
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def ex_mat(rows):
|
|
81
|
+
out = []
|
|
82
|
+
ctx = re.compile(r"材质")
|
|
83
|
+
tok = re.compile(r"\bM_\w+")
|
|
84
|
+
for i, ln in rows:
|
|
85
|
+
if not ctx.search(ln):
|
|
86
|
+
continue
|
|
87
|
+
for m in tok.finditer(ln):
|
|
88
|
+
out.append((m.group(0), i))
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ex_fmt(rows):
|
|
93
|
+
out = []
|
|
94
|
+
ctx = re.compile(r"交付形态|导出格式|export_format|交付件|导出参数")
|
|
95
|
+
tok = re.compile(r"\b(FBX|GLB|glTF|USD|ABC)\b")
|
|
96
|
+
primary = re.compile(r"为主")
|
|
97
|
+
for i, ln in rows:
|
|
98
|
+
if not ctx.search(ln):
|
|
99
|
+
continue
|
|
100
|
+
found = tok.findall(ln)
|
|
101
|
+
if not found:
|
|
102
|
+
continue
|
|
103
|
+
if primary.search(ln):
|
|
104
|
+
out.append((found[0], i))
|
|
105
|
+
else:
|
|
106
|
+
for f in found:
|
|
107
|
+
out.append((f, i))
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def ex_asset(rows):
|
|
112
|
+
out = []
|
|
113
|
+
tok = re.compile(r"\b(SM_\w+|SK_\w+)")
|
|
114
|
+
for i, ln in rows:
|
|
115
|
+
for m in tok.finditer(ln):
|
|
116
|
+
out.append((m.group(0).rstrip("_LOD123456789"), i))
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
CATEGORIES = [
|
|
121
|
+
("size", "尺寸", ex_size),
|
|
122
|
+
("tris", "面数预算", ex_tris),
|
|
123
|
+
("uv", "UV 通道", ex_uv),
|
|
124
|
+
("mat", "材质名", ex_mat),
|
|
125
|
+
("fmt", "导出格式", ex_fmt),
|
|
126
|
+
("asset", "资产名", ex_asset),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def extract(path):
|
|
131
|
+
rows = lines_of(Path(path).read_text(encoding="utf-8"))
|
|
132
|
+
return {key: fn(rows) for key, _label, fn in CATEGORIES}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def compare(pa, pb, verbose=True):
|
|
136
|
+
"""返回矛盾列表 [(category, label, valsA, linesA, valsB, linesB), ...]。"""
|
|
137
|
+
ea, eb = extract(pa), extract(pb)
|
|
138
|
+
bad = []
|
|
139
|
+
for key, label, _fn in CATEGORIES:
|
|
140
|
+
va = sorted(set(v for v, _i in ea[key]))
|
|
141
|
+
vb = sorted(set(v for v, _i in eb[key]))
|
|
142
|
+
if not va or not vb:
|
|
143
|
+
continue
|
|
144
|
+
if set(va).isdisjoint(set(vb)):
|
|
145
|
+
la = sorted(set(i for _v, i in ea[key]))
|
|
146
|
+
lb = sorted(set(i for _v, i in eb[key]))
|
|
147
|
+
bad.append((key, label, va, la, vb, lb))
|
|
148
|
+
if verbose:
|
|
149
|
+
print("[矛盾] %s:%s %s(%s 行 %s)↔ %s %s(%s 行 %s)" % (
|
|
150
|
+
label, Path(pa).name, "/".join(va), Path(pa).name, ",".join(map(str, la)),
|
|
151
|
+
Path(pb).name, "/".join(vb), Path(pb).name, ",".join(map(str, lb))))
|
|
152
|
+
return bad
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# —— 自测 fixtures:5 处真实矛盾(2026-11-02 实机测试,欧文表 vs 西奥表) ————
|
|
156
|
+
|
|
157
|
+
FIX_A = """# 自测件 A(溯源:欧文_表格.md 五矛盾缩样)
|
|
158
|
+
| 项 | 值 |
|
|
159
|
+
|---|---|
|
|
160
|
+
| 单位 | 场景单位 1.0 = 1 米;箱子尺寸假设 0.6×0.6×0.6 m |
|
|
161
|
+
| 导出参数 export_format | 'GLB' |
|
|
162
|
+
| 交付件 | SM_Crate_Wood_01.glb(GLB 自包含) |
|
|
163
|
+
| 材质 | `M_Crate_Wood` |
|
|
164
|
+
| 面数 | 三角面 ≤ 800(LOD0) |
|
|
165
|
+
| UV 通道 | 仅 UV0 单通道 |
|
|
166
|
+
| 资产 | SM_Crate_Wood_01 |
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
FIX_B = """# 自测件 B(溯源:西奥_表格_v2.md 五矛盾缩样)
|
|
170
|
+
| 项 | 值 |
|
|
171
|
+
|---|---|
|
|
172
|
+
| 交付形态 | FBX + 贴图为主(Unity 原生导入);GLB 备选 |
|
|
173
|
+
| 资产清单 | SM_Crate_Wood_01,目标 300 / 上限 400 |
|
|
174
|
+
| model_spec | SM_Crate_Wood_01,尺寸 0.80 × 0.80 × 0.80,三角面上限 ≤ 400,UV0 + UV1 |
|
|
175
|
+
| 材质槽 | 1(M_Crate_Wood_01) |
|
|
176
|
+
| 外廓 | 外廓仍 0.80 m 等边 |
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
FIX_CLEAN_A = """# 自测件(正常对 A)
|
|
180
|
+
| 项 | 值 |
|
|
181
|
+
|---|---|
|
|
182
|
+
| 资产 | SM_Crate_Wood_01 |
|
|
183
|
+
| 尺寸 | 0.8×0.8×0.8 m |
|
|
184
|
+
| 面数 | 三角面 ≤ 800 |
|
|
185
|
+
| UV | 仅 UV0 单通道 |
|
|
186
|
+
| 材质 | M_Crate_Wood_01 |
|
|
187
|
+
| 导出 | 导出参数 export_format 'GLB';交付件 SM_Crate_Wood_01.glb |
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
FIX_CLEAN_B = """# 自测件(正常对 B)
|
|
191
|
+
| 项 | 值 |
|
|
192
|
+
|---|---|
|
|
193
|
+
| 资产清单 | SM_Crate_Wood_01 |
|
|
194
|
+
| 尺寸 | 0.80 × 0.80 × 0.80 |
|
|
195
|
+
| 面数 | 目标 600 / 上限 800 |
|
|
196
|
+
| UV 通道 | UV0 单通道 |
|
|
197
|
+
| 材质槽 | M_Crate_Wood_01 |
|
|
198
|
+
| 交付形态 | GLB 为主 |
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def selftest():
|
|
203
|
+
d = Path(tempfile.mkdtemp(prefix="xcheck_"))
|
|
204
|
+
a, b = d / "A.md", d / "B.md"
|
|
205
|
+
a.write_text(FIX_A, encoding="utf-8")
|
|
206
|
+
b.write_text(FIX_B, encoding="utf-8")
|
|
207
|
+
bad = compare(str(a), str(b), verbose=True)
|
|
208
|
+
keys = set(k for k, *_ in bad)
|
|
209
|
+
want = {"size", "tris", "uv", "mat", "fmt"}
|
|
210
|
+
ok1 = want <= keys
|
|
211
|
+
print("真阳性(5 真实矛盾):%s → %s" % (sorted(keys), "全中 ✅" if ok1 else "漏报 ❌"))
|
|
212
|
+
ca, cb = d / "clean_A.md", d / "clean_B.md"
|
|
213
|
+
ca.write_text(FIX_CLEAN_A, encoding="utf-8")
|
|
214
|
+
cb.write_text(FIX_CLEAN_B, encoding="utf-8")
|
|
215
|
+
bad2 = compare(str(ca), str(cb), verbose=False)
|
|
216
|
+
ok2 = not bad2
|
|
217
|
+
print("正常对误报:%d 条 → %s" % (len(bad2), "0 误报 ✅" if ok2 else "误报 ❌ %s" % bad2))
|
|
218
|
+
if ok1 and ok2:
|
|
219
|
+
print("SELFTEST PASS(真阳性全中 + 正常表 0 误报)")
|
|
220
|
+
return 0
|
|
221
|
+
print("SELFTEST FAIL")
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main(argv):
|
|
226
|
+
if len(argv) == 2 and argv[1] == "--selftest":
|
|
227
|
+
return selftest()
|
|
228
|
+
if len(argv) != 3:
|
|
229
|
+
print(__doc__)
|
|
230
|
+
return 2
|
|
231
|
+
bad = compare(argv[1], argv[2])
|
|
232
|
+
if bad:
|
|
233
|
+
print("共 %d 处矛盾——不许宣称「表已咬合」;改到一致或标「待拍板」。" % len(bad))
|
|
234
|
+
return 1
|
|
235
|
+
print("咬合 ✅(六类可机查项双方声明一致;七项中的「交付形态主/备件」含在 fmt)")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""离线模板自检(blender-studio v1.3.4)。
|
|
3
|
+
|
|
4
|
+
## 这是什么、不是什么
|
|
5
|
+
|
|
6
|
+
**不是生成器**——不产出任何内容,只回答一件事:
|
|
7
|
+
**包里这四份模板,是不是都能真的填、且彼此不打架、SKILL 里也指得到?**
|
|
8
|
+
|
|
9
|
+
为什么需要:这一版把「没装 Blender 也能交付」落成了四份表。模板最容易出的问题不是缺失,
|
|
10
|
+
而是**「在但没用」**——表头列数与示例行对不上(填进去就错位)、SKILL 里没引用(agent 想不到调)。
|
|
11
|
+
任何一条都会让买家拿到「看起来有、实际不能用」的东西。
|
|
12
|
+
|
|
13
|
+
## 判据
|
|
14
|
+
|
|
15
|
+
1. 六份模板都在且非空
|
|
16
|
+
2. 每张 md 表的**表头列数 == 分隔行列数 == 示例行列数**(对不上就会填错位)
|
|
17
|
+
3. 每份顶部都有「本表不需要 Blender 即可填写交付」声明
|
|
18
|
+
4. SKILL 里确实引用了六个路径(防「模板在但没人用」)
|
|
19
|
+
5. **反向**:删一份模板 / 把示例行列数改坏 → 必须报错
|
|
20
|
+
(只做正向断言,判据坏了也照样绿)
|
|
21
|
+
"""
|
|
22
|
+
import pathlib
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
try: # 控制台编码兜底:中文 Windows 控制台是 cp936,打不出 ✅/❌
|
|
27
|
+
import _console
|
|
28
|
+
|
|
29
|
+
_console.ensure_utf8()
|
|
30
|
+
except Exception: # noqa: BLE001
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
PKG = pathlib.Path(__file__).resolve().parents[1]
|
|
34
|
+
TPL = {
|
|
35
|
+
"asset_list.md": "资产清单表",
|
|
36
|
+
"model_spec.md": "建模参数表",
|
|
37
|
+
"naming_export.md": "命名与导出规范",
|
|
38
|
+
"delivery_checklist.md": "交付检查表",
|
|
39
|
+
"uv_unwrap.md": "UV 展开打包工序单",
|
|
40
|
+
"lighting_exposure.md": "布光/曝光工序单",
|
|
41
|
+
}
|
|
42
|
+
DECL = "不需要 Blender 即可填写交付"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tables(path: pathlib.Path) -> list[tuple[list[str], list[str], list[str], int]]:
|
|
46
|
+
"""→ [(表头列, 分隔行列, 示例行列, 行号)];只取带分隔行(|---|)的真表。"""
|
|
47
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
48
|
+
out = []
|
|
49
|
+
for i in range(len(lines) - 2):
|
|
50
|
+
head, sep, first = lines[i].strip(), lines[i + 1].strip(), lines[i + 2].strip()
|
|
51
|
+
if not (head.startswith("|") and sep.startswith("|") and first.startswith("|")):
|
|
52
|
+
continue
|
|
53
|
+
if not re.fullmatch(r"\|[\s:|-]+\|", sep):
|
|
54
|
+
continue
|
|
55
|
+
cell = lambda s: [c for c in s.strip().strip("|").split("|")]
|
|
56
|
+
out.append((cell(head), cell(sep), cell(first), i + 1))
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_all(root: pathlib.Path | None = None) -> list[str]:
|
|
61
|
+
"""→ 问题清单(空 = 通过)。root 可替换,便于反向测试。"""
|
|
62
|
+
root = root or PKG
|
|
63
|
+
tdir = root / "templates"
|
|
64
|
+
bad: list[str] = []
|
|
65
|
+
|
|
66
|
+
# ① 四份都在且非空
|
|
67
|
+
for name, label in TPL.items():
|
|
68
|
+
f = tdir / name
|
|
69
|
+
if not f.is_file():
|
|
70
|
+
bad.append(f"缺模板:{name}({label})")
|
|
71
|
+
elif not f.read_text(encoding="utf-8").strip():
|
|
72
|
+
bad.append(f"模板为空:{name}({label})")
|
|
73
|
+
if bad:
|
|
74
|
+
return bad
|
|
75
|
+
|
|
76
|
+
# ② 表头 / 分隔行 / 示例行列数一致
|
|
77
|
+
for name in TPL:
|
|
78
|
+
tabs = _tables(tdir / name)
|
|
79
|
+
if not tabs:
|
|
80
|
+
bad.append(f"{name} 里没有可填的表(没找到「表头+分隔行+示例行」)")
|
|
81
|
+
continue
|
|
82
|
+
for head, sep, first, ln in tabs:
|
|
83
|
+
if not (len(head) == len(sep) == len(first)):
|
|
84
|
+
bad.append(f"{name}:{ln} 列数不一致:表头 {len(head)} / 分隔行 {len(sep)} / "
|
|
85
|
+
f"示例行 {len(first)}(填进去会错位)")
|
|
86
|
+
|
|
87
|
+
# ③ 顶部声明
|
|
88
|
+
for name in TPL:
|
|
89
|
+
head_text = "\n".join((tdir / name).read_text(encoding="utf-8").splitlines()[:6])
|
|
90
|
+
if DECL not in head_text:
|
|
91
|
+
bad.append(f"{name} 顶部缺「{DECL}」声明")
|
|
92
|
+
|
|
93
|
+
# ④ SKILL 引用
|
|
94
|
+
corpus = "\n".join(s.read_text(encoding="utf-8")
|
|
95
|
+
for s in (root / "skills").rglob("SKILL.md"))
|
|
96
|
+
for name in TPL:
|
|
97
|
+
if name not in corpus:
|
|
98
|
+
bad.append(f"没有任何 SKILL.md 引用 templates/{name}(模板在但没人用)")
|
|
99
|
+
|
|
100
|
+
return bad
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main() -> int:
|
|
104
|
+
if "--selftest" not in sys.argv:
|
|
105
|
+
bad = check_all()
|
|
106
|
+
if bad:
|
|
107
|
+
print("❌ 模板自检未通过:")
|
|
108
|
+
for b in bad:
|
|
109
|
+
print(f" · {b}")
|
|
110
|
+
return 1
|
|
111
|
+
print("✅ 模板自检通过:六份齐全、表头与示例对齐、顶部声明在、SKILL 已引用")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
print("==== 离线模板自检(blender-studio)====")
|
|
115
|
+
fails, checks = [], []
|
|
116
|
+
|
|
117
|
+
def ck(label, cond, detail=""):
|
|
118
|
+
checks.append(label)
|
|
119
|
+
print((" ✅ " if cond else " ❌ ") + label + ("" if cond else f" —— {detail}"))
|
|
120
|
+
if not cond:
|
|
121
|
+
fails.append(label)
|
|
122
|
+
|
|
123
|
+
bad = check_all()
|
|
124
|
+
ck("① 六份模板齐全且非空", not [b for b in bad if "缺模板" in b or "模板为空" in b],
|
|
125
|
+
[b for b in bad if "模板" in b][:3])
|
|
126
|
+
ck("② 表头列数 == 示例行列数(每张表)",
|
|
127
|
+
not [b for b in bad if "列 vs 示例行" in b or "没有可填的表" in b],
|
|
128
|
+
[b for b in bad if "列" in b][:3])
|
|
129
|
+
ck("③ 六份顶部都有「不需要 Blender 即可填写交付」声明",
|
|
130
|
+
not [b for b in bad if "声明" in b], [b for b in bad if "声明" in b][:3])
|
|
131
|
+
ck("④ SKILL 引用了六个模板路径", not [b for b in bad if "SKILL" in b],
|
|
132
|
+
[b for b in bad if "SKILL" in b][:3])
|
|
133
|
+
|
|
134
|
+
# 反向:判据必须能失败
|
|
135
|
+
import shutil
|
|
136
|
+
import tempfile
|
|
137
|
+
tmp = pathlib.Path(tempfile.mkdtemp(prefix="btpl_"))
|
|
138
|
+
try:
|
|
139
|
+
shutil.copytree(PKG / "templates", tmp / "templates")
|
|
140
|
+
shutil.copytree(PKG / "skills", tmp / "skills")
|
|
141
|
+
|
|
142
|
+
(tmp / "templates" / "model_spec.md").unlink()
|
|
143
|
+
ck("⑤ 反向:删一份模板 → 必须报错", bool(check_all(tmp)), "删了却没报错")
|
|
144
|
+
shutil.copy(PKG / "templates" / "model_spec.md", tmp / "templates" / "model_spec.md")
|
|
145
|
+
|
|
146
|
+
_f = tmp / "templates" / "asset_list.md"
|
|
147
|
+
_t = _f.read_text(encoding="utf-8")
|
|
148
|
+
# 把某张表的分隔行多切一列 → 表头与示例行列数仍相等,但分隔行不一致(格错位)
|
|
149
|
+
_t = _t.replace("|---|---|---|---|---|---|---|", "|---|---|---|---|---|---|---|---|", 1)
|
|
150
|
+
_f.write_text(_t, encoding="utf-8")
|
|
151
|
+
ck("⑥ 反向:分隔行列数与表头不一致 → 必须报错", bool(check_all(tmp)), "列数错了却没报错")
|
|
152
|
+
finally:
|
|
153
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
154
|
+
|
|
155
|
+
print()
|
|
156
|
+
print(f"断言数:{len(checks)}")
|
|
157
|
+
if fails:
|
|
158
|
+
print(f"❌ {len(fails)} 项失败:{fails}")
|
|
159
|
+
return 1
|
|
160
|
+
print("── 离线模板自检全部通过")
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
sys.exit(main())
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 资产清单表
|
|
2
|
+
|
|
3
|
+
> **本表不需要 Blender 即可填写交付;填完即为可交产物。**
|
|
4
|
+
> 用法:一行一个资产,先填「资产名 / 类型 / 用途」,再按用途给面数预算与命名。
|
|
5
|
+
> 填完连同 `model_spec.md` / `naming_export.md` / `delivery_checklist.md` 一起交付。
|
|
6
|
+
|
|
7
|
+
## 一 项目基本信息
|
|
8
|
+
|
|
9
|
+
| 项 | 填写 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 项目 / 包名 | 【填】如 `lowpoly_props_pack_01` |
|
|
12
|
+
| 交付形态 | 【填】单个 blend / glTF + 贴图 / 两者都要 |
|
|
13
|
+
| 目标平台 | 【填】Unity / Unreal / 网页 / 移动端 |
|
|
14
|
+
| 渲染风格 | 【填】low-poly / 写实 / 卡通 |
|
|
15
|
+
| 单位约定 | 【填】1 单位 = 1 米(默认) |
|
|
16
|
+
|
|
17
|
+
## 二 资产清单
|
|
18
|
+
|
|
19
|
+
> 面数预算指**三角面**(tri),不是四边面。超出预算的资产必须写明原因。
|
|
20
|
+
|
|
21
|
+
| # | 资产名 | 类型 | 用途 | 面数预算(tri) | 命名 | 备注 |
|
|
22
|
+
|---|---|---|---|---|---|---|
|
|
23
|
+
| 1 | 【填】SM_Sword_Proxy_01 | 道具 | 主手武器,第一人称可见 | 【填】≤ 800 | `SM_<名>_<变体>` | 【填】需 LOD1 |
|
|
24
|
+
| 2 | 【填】SM_Crate_Wood_01 | 道具 | 场景可破坏物 | 【填】≤ 300 | 同上 | 【填】 |
|
|
25
|
+
| 3 | 【填】ENV_Room_Corner_01 | 环境 | 模块化场景拼块 | 【填】≤ 2000 | `ENV_<名>_<尺寸>` | 【填】贴图可复用 |
|
|
26
|
+
| 4 | 【填】CH_NPC_Merchant_01 | 角色 | 对话 NPC,不参与战斗 | 【填】≤ 6000 | `CH_<名>_<编号>` | 【填】需骨骼 |
|
|
27
|
+
| — | **合计** | — | — | **【填】** | — | — |
|
|
28
|
+
|
|
29
|
+
## 三 类型说明(照这个口径填,别自己造)
|
|
30
|
+
|
|
31
|
+
| 类型 | 前缀 | 典型面数 | 是否含骨骼 |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| 道具(Prop) | `SM_` | 100–2000 | 否 |
|
|
34
|
+
| 环境(Environment) | `ENV_` | 500–20000 | 否 |
|
|
35
|
+
| 角色(Character) | `CH_` | 3000–50000 | 是 |
|
|
36
|
+
|
|
37
|
+
## 四 缺失信息清单(没有就写"待客户确认",别猜)
|
|
38
|
+
|
|
39
|
+
| 项 | 状态 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 参考图 / 概念稿 | 【填】已提供 / 待提供 |
|
|
42
|
+
| 尺寸参考(真实尺寸或比例) | 【填】 |
|
|
43
|
+
| 数量与变体(是否要换色/换材质) | 【填】 |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 交付检查表
|
|
2
|
+
|
|
3
|
+
> **本表不需要 Blender 即可填写交付;填完即为可交产物。**
|
|
4
|
+
> 用法:交付前逐项打勾。**没做过的项不能打勾**——宁可写「未做,原因:」,也不要空着或写「已检查」。
|
|
5
|
+
|
|
6
|
+
## 一 资产与命名
|
|
7
|
+
|
|
8
|
+
| # | 检查项 | 判据 | 结果 |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| 1 | 命名全部符合前缀规则 | 无中文、无空格、无 `final`/`副本` | 【填】✅ / ❌ |
|
|
11
|
+
| 2 | 资产数量与 `asset_list.md` 一致 | 行数对得上,无遗漏或多出 | 【填】 |
|
|
12
|
+
| 3 | 集合/层级按类型分组 | 道具/环境/角色分开 | 【填】 |
|
|
13
|
+
| 4 | 无隐藏的无用物体 | 空物体、冻结的参考图已删 | 【填】 |
|
|
14
|
+
|
|
15
|
+
## 二 面数与几何
|
|
16
|
+
|
|
17
|
+
| # | 检查项 | 判据 | 结果 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| 5 | 每个资产面数 ≤ 预算 | 与 `model_spec.md` 逐行对照 | 【填】 |
|
|
20
|
+
| 6 | 已三角化(导出前) | 导出 glb 后读回检查 | 【填】 |
|
|
21
|
+
| 7 | 变换已应用 | 位置/旋转/缩放均为 1.0 / 0 | 【填】 |
|
|
22
|
+
| 8 | 原点位置符合约定 | 中心 / 底部 / 轴心(见规范) | 【填】 |
|
|
23
|
+
| 9 | 无重叠面、无翻转法线 | 视图层里看背面剔除 | 【填】 |
|
|
24
|
+
|
|
25
|
+
## 三 UV 与材质
|
|
26
|
+
|
|
27
|
+
| # | 检查项 | 判据 | 结果 |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| 10 | UV 无重叠(除镜像) | UV 编辑器重叠检查 | 【填】 |
|
|
30
|
+
| 11 | 贴图密度一致 | 各资产 texel 密度接近 | 【填】 |
|
|
31
|
+
| 12 | 材质槽数与规范一致 | 与 `model_spec.md` 对照 | 【填】 |
|
|
32
|
+
| 13 | 贴图命名 `T_` 前缀 | 与 `naming_export.md` 一致 | 【填】 |
|
|
33
|
+
| 14 | 无法线贴图时切线已关 | 避免无谓体积 | 【填】 |
|
|
34
|
+
|
|
35
|
+
## 四 导出可回读(**这一项最容易被跳过**)
|
|
36
|
+
|
|
37
|
+
| # | 检查项 | 判据 | 结果 |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| 15 | glb/gltf 能导入空场景 | New File → Import,无报错 | 【填】 |
|
|
40
|
+
| 16 | 尺寸与朝向正确 | 量包围盒、确认 +Y up | 【填】 |
|
|
41
|
+
| 17 | 材质与贴图完整 | 导入后颜色/法线都在 | 【填】 |
|
|
42
|
+
| 18 | 动画(若含)能播放 | 时间轴播放无错位 | 【填】 |
|
|
43
|
+
| 19 | 文件体积在预期内 | 与预算对照,异常大先查贴图 | 【填】 |
|
|
44
|
+
|
|
45
|
+
## 五 交付包
|
|
46
|
+
|
|
47
|
+
| # | 检查项 | 判据 | 结果 |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| 20 | 目录结构符合规范 | `source/` `export/` `textures/` `docs/` | 【填】 |
|
|
50
|
+
| 21 | 四份文档齐全 | asset_list / model_spec / naming_export / 本表 | 【填】 |
|
|
51
|
+
| 22 | 预览图已出 | 至少一张,命名 preview_编号.png(例 preview_01.png) | 【填】 |
|
|
52
|
+
| 23 | 无临时文件 | 无 `.blend1`、`.DS_Store`、`新建文件夹` | 【填】 |
|
|
53
|
+
|
|
54
|
+
## 六 签字
|
|
55
|
+
|
|
56
|
+
| 项 | 填写 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| 检查人 | 【填】 |
|
|
59
|
+
| 检查日期 | 【填】 |
|
|
60
|
+
| 未完成项及原因 | 【填】如「第 18 项未做:本包不含动画」 |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 布光/曝光工序单(三点布光 → 曝光校准 → 渲染观感自检)
|
|
2
|
+
|
|
3
|
+
> 本表不需要 Blender 即可填写交付(执行工序时才开 Blender)。配方从 C4 成品渲染(✅6/6)抽成,复用到材质预览/资产展示渲染。
|
|
4
|
+
|
|
5
|
+
## 1. 三点布光(照抄 C4 配方,锚 C4 EEVEE/Cycles 占比 0.68 ✓)
|
|
6
|
+
|
|
7
|
+
| 灯 | 类型 | 位置 | 能量 | 尺寸 | 要点 |
|
|
8
|
+
|---|---|---|---|---|---|
|
|
9
|
+
| 主光 | SUN | (4, -6, 8) | 3.0 | — | 斜上方 45° 打主体 |
|
|
10
|
+
| 补光 | AREA | (-5, -3, 3) | 40.0 | 4.0 | 补暗部,尺寸大则阴影软 |
|
|
11
|
+
| 轮廓光 | AREA | (5, 4, 2) | 15.0 | 4.0 | 从后侧勾边 |
|
|
12
|
+
|
|
13
|
+
- **灯光默认朝 -Z,必须逐灯旋转指向目标点**(`to_track_quat("-Z","Y")`),否则侧面全黑。
|
|
14
|
+
- 相机参考(单物体):loc=(3.0, -3.2, 2.1),lens=60,指向原点。
|
|
15
|
+
- `film_transparent = True`(透明背景,方便量构图占比/合成)。
|
|
16
|
+
|
|
17
|
+
## 2. 曝光/观感验收线(写死)
|
|
18
|
+
|
|
19
|
+
| 判据 | 阈值 | 出处 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 明暗层次(luma_std) | **> 0.02** | C2 判据范式;缺布光实测 0.012 全黑 |
|
|
22
|
+
| 明暗跨度(luma_max − luma_min) | **> 0.08** | C2 判据范式;缺布光实测 0.006 |
|
|
23
|
+
| 非纯色(unique_rgb_ratio) | > 0.05 | C2 判据范式 |
|
|
24
|
+
| EEVEE/Cycles 占比 | ≥ 0.6 | C4 实测 0.68 ✓ |
|
|
25
|
+
|
|
26
|
+
## 3. 填写(模型资产渲染交付时)
|
|
27
|
+
|
|
28
|
+
| 项目 | 值 | 备注 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 资产名 | | 与 asset_list.md 对齐 |
|
|
31
|
+
| 用途 | | 材质预览/成品渲染/展示图 |
|
|
32
|
+
| 三灯能量(主/补/轮廓) | / / | 默认 3.0 / 40.0 / 15.0 |
|
|
33
|
+
| luma_std 实测 | | 必须 > 0.02 |
|
|
34
|
+
| 明暗跨度实测 | | 必须 > 0.08 |
|
|
35
|
+
|
|
36
|
+
## 4. 常见翻车
|
|
37
|
+
|
|
38
|
+
- 只加灯不旋转指向目标 → 侧面全黑(灯光默认朝 -Z)。
|
|
39
|
+
- 没灯直接渲染 → luma_std 0.012、mean 0.006,判据必挂(C2 现状)。
|
|
40
|
+
- 单灯硬光 → 明暗跨度够但 luma_std 偏低,按 §1 三灯配齐。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 建模参数表
|
|
2
|
+
|
|
3
|
+
> **本表不需要 Blender 即可填写交付;填完即为可交产物。**
|
|
4
|
+
> 用法:与 `asset_list.md` 一一对应,一个资产一行。**参数要能给到建模的人直接照着做**。
|
|
5
|
+
|
|
6
|
+
## 一 全局约定
|
|
7
|
+
|
|
8
|
+
| 项 | 值 |
|
|
9
|
+
|---|---|
|
|
10
|
+
| 单位 | 【填】米(Blender 默认 1 unit = 1 m) |
|
|
11
|
+
| 朝向 | 【填】+Y up(导出时);Blender 内 -Y 朝前 |
|
|
12
|
+
| 缩放 | 【填】`Ctrl+A → Scale` 必须应用(1.0,1.0,1.0) |
|
|
13
|
+
| 变换 | 【填】原点在世界中心 / 物体底部 / 轴心 |
|
|
14
|
+
| 三角化 | 【填】导出前三角化(glTF 必须) |
|
|
15
|
+
|
|
16
|
+
## 二 分资产参数
|
|
17
|
+
|
|
18
|
+
| # | 资产名 | 尺寸(X×Y×Z, 米) | 三角面上限 | UV 通道 | 材质槽数 | LOD 级数 | 备注 |
|
|
19
|
+
|---|---|---|---|---|---|---|---|
|
|
20
|
+
| 1 | 【填】SM_Sword_Proxy_01 | 【填】0.06 × 1.10 × 0.12 | 【填】800 | UV0(贴图) | 1 | 【填】2(LOD0/LOD1) | 【填】握柄与刃口分开材质 |
|
|
21
|
+
| 2 | 【填】SM_Crate_Wood_01 | 【填】0.80 × 0.80 × 0.80 | 【填】300 | UV0 | 1 | 1 | 【填】 |
|
|
22
|
+
| 3 | 【填】ENV_Room_Corner_01 | 【填】4.00 × 4.00 × 3.00 | 【填】2000 | UV0 + UV1(光照图) | 2 | 【填】2 | 【填】模块化,4 边可拼 |
|
|
23
|
+
| 4 | 【填】CH_NPC_Merchant_01 | 【填】0.60 × 0.45 × 1.75 | 【填】6000 | UV0 | 3 | 【填】3 | 【填】含骨骼,无表情 |
|
|
24
|
+
|
|
25
|
+
## 三 材质与贴图
|
|
26
|
+
|
|
27
|
+
| 项 | 填写 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 贴图尺寸 | 【填】2048×2048(主体)/ 1024(小件) |
|
|
30
|
+
| 贴图类型 | 【填】BaseColor + Normal + ORM(R=AO/G=Rough/B=Metal) |
|
|
31
|
+
| 是否共用图集 | 【填】是 / 否;若共用,列出同图集资产 |
|
|
32
|
+
| 透明 | 【填】无 / Alpha Clip / Alpha Blend(**移动端尽量不用 Blend**) |
|
|
33
|
+
|
|
34
|
+
## 四 建模要点(给动手的人)
|
|
35
|
+
|
|
36
|
+
1. **先做 blockout 再细化**:比例对不上时,细化越细返工越大
|
|
37
|
+
2. **保持四边面**,只在硬表面棱边加支撑线;导出前统一三角化
|
|
38
|
+
3. **法线**:硬表面用自定义法线或加权重法线,别直接 Shade Smooth
|
|
39
|
+
4. **UV**:接缝放在背面或不常看的一侧;贴图密度各资产之间尽量一致
|
|
40
|
+
5. **命名与集合**:按 `naming_export.md` 的前缀分集合,方便一次选中导出
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 命名与导出规范
|
|
2
|
+
|
|
3
|
+
> **本表不需要 Blender 即可填写交付;填完即为可交产物。**
|
|
4
|
+
> 用法:先定命名前缀,再核对导出参数。**导出的东西别人要能一次导进去,参数必须写全**。
|
|
5
|
+
|
|
6
|
+
## 一 命名规则
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
<前缀>_<主体名>_<变体/尺寸>[_LOD<级别>]
|
|
10
|
+
例:SM_Sword_Proxy_01 ENV_Room_Corner_4x4_01_LOD1
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| 项 | 规则 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 前缀 | 【填】`SM_`(道具)/ `ENV_`(环境)/ `CH_`(角色)/ `MAT_`(材质)/ `T_`(贴图) |
|
|
16
|
+
| 大小写 | 【填】全大写主体 + 首字母大写变体(**不用中文、不用空格**) |
|
|
17
|
+
| 分隔符 | 【填】下划线 `_`,不用 `-`、不用 `.` |
|
|
18
|
+
| 变体编号 | 【填】两位数字 `01` 起 |
|
|
19
|
+
| LOD 后缀 | 【填】`_LOD0/_LOD1/...`,**LOD0 是最高精度** |
|
|
20
|
+
| 禁止 | 【填】`final`、`最终版`、`copy`、`new`、个人缩写 |
|
|
21
|
+
|
|
22
|
+
## 二 导出参数(glTF / glb)
|
|
23
|
+
|
|
24
|
+
| 参数 | 值 | 为什么 |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| 格式 | 【填】`.glb`(单文件)/ `.gltf + .bin + 贴图` | 【填】glb 便于分发,gltf 便于改 |
|
|
27
|
+
| +Y up | 【填】**开启**(Apply +Y up) | 引擎与 glTF 标准都是 Y 轴向上 |
|
|
28
|
+
| 单位 | 【填】米(Scale 1.0) | 【填】Unity/UE 导入时按 1 米对应 |
|
|
29
|
+
| 应用变换 | 【填】**导出前必做**(位置/旋转/缩放归零) | 否则引擎里尺寸错乱 |
|
|
30
|
+
| 三角化 | 【填】开启 | glTF 只支持三角面 |
|
|
31
|
+
| 法线 | 【填】导出 | 【填】不做则引擎自己算法线,硬表面会脏 |
|
|
32
|
+
| 切线 | 【填】开启(有法线贴图时必须) | 无法线贴图则不需要 |
|
|
33
|
+
| 动画 | 【填】含 / 不含 | 【填】角色含骨骼动画,道具不含 |
|
|
34
|
+
| 材质 | 【填】导出 Principled BSDF → PBR | 【填】复杂节点树不会被导出 |
|
|
35
|
+
| 贴图 | 【填】嵌进 glb / 单独放 textures/ | 【填】 |
|
|
36
|
+
| 压缩 | 【填】不压缩 / Draco(注明需引擎支持) | 【填】Draco 能小 60%,但部分旧引擎不支持 |
|
|
37
|
+
|
|
38
|
+
## 三 交付文件结构
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
【填】包名/
|
|
42
|
+
├─ source/ # 源文件
|
|
43
|
+
│ └─ 【填】包名.blend
|
|
44
|
+
├─ export/
|
|
45
|
+
│ ├─ 【填】SM_xxx.glb
|
|
46
|
+
│ └─ 【填】CH_xxx.glb
|
|
47
|
+
├─ textures/
|
|
48
|
+
│ └─ 【填】T_xxx_basecolor.png
|
|
49
|
+
└─ docs/
|
|
50
|
+
├─ asset_list.md / model_spec.md
|
|
51
|
+
├─ naming_export.md / delivery_checklist.md
|
|
52
|
+
└─ preview_01.png(预览图)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**规则**:源文件与导出物**分开放**;贴图单独目录;文档随包;**不要出现 `新建文件夹`、`副本`**。
|
|
56
|
+
|
|
57
|
+
## 四 导出后必做的三件
|
|
58
|
+
|
|
59
|
+
1. **重新导入一次**(New File → Import glTF),确认能读、尺寸与朝向正确
|
|
60
|
+
2. **检查材质是否丢**(贴图路径、Principled 参数)
|
|
61
|
+
3. **量一下面数与包围盒**,与 `model_spec.md` 的上限对齐
|