上一节把美术生产链路拆成了四道门,并说清一件事:判定必须有凭据。可凭据齐了还不够——还得有人对「齐了没」负责。本节解决的就是这个:同一件交付物,谁执行、谁会签、谁最终拍板。这三件事如果没有唯一定义,链路再清楚也会在「到底听谁的」上停摆。
角色不是头衔,是判定权
先把一个误区拆掉:分工不是「谁画什么」,而是「谁对什么有最终判定权」。一个团队可以有三个人都叫「美术」,但每件交付物只能有一个最终拍板人。否则你会看到最典型的死锁:主美说这版过了,总监说不行,制作等指令,排期空转一周。
| 角色 | 主要交付物 | 判定权 |
|---|---|---|
美术总监 art-director | 风格指南 | 风格方向最终拍板 |
主美 lead-artist | 概念 / 模型 / 贴图 / 动画 / UI 的通过 | 美术质量最终拍板 |
概念设计 concept-artist | 参考板、风格帧 | 出方案,无最终拍板 |
3D 模型 modeler | 模型 | 执行,无最终拍板 |
贴图 texture-artist | 贴图 | 执行,无最终拍板 |
动画 animator | 动画 | 执行,无最终拍板 |
UI 美术 ui-artist | UI 套件 | 执行,无最终拍板 |
特效美术 vfx-artist | 特效 | 执行,无最终拍板 |
技术美术 tech-artist | 资产清单、特效规格 | 技术可行性与规格最终拍板 |
美术 PM art-pm | 外包批次 | 外包验收最终拍板 |
程序 engineer | —— | 接入可行性会签 |
这张表里最容易被忽略的是技术美术与美术 PM 也有拍板权——只是拍的板不同:TA 拍「规格能不能成立」,PM 拍「外包件收不收」。拍板权分散在多个维度,但每个维度唯一,这是矩阵能成立的前提。
职责矩阵:A / R / C / I 四个字母
把「谁拍板」这件事写清楚,业界通用的是 RACI 四个字母:
| 字母 | 含义 | 在美术团队里是什么 |
|---|---|---|
| A Accountable | 最终拍板,每行唯一 | 「这版过不过」由他说了算 |
| R Responsible | 执行,真正动手的人 | 画 / 建模 / 做动画的人 |
| C Consulted | 会签,拍板前必须问 | 规格、预算、接入可行性 |
| I Informed | 知会,事后告知即可 | 排期、下游依赖方 |
四个字母里唯一有硬约束的是 A:它必须每行唯一。R 可以多人,C、I 可以多人,A 只能一个。下面这段是 D 层矩阵配置,把这张表落成可被校验器消费的数据。
{
"version": 1,
"project": "stardust-inn",
"note": "角色职责矩阵(RACI)。accountable=最终拍板(每行唯一,禁双主);responsible=执行;consulted=会签;informed=知会。这是「谁判定」的唯一真源:P 层 TS 内核必须与本表逐行一致,漂移即测试失败。口径为可复现的组织模型;真实团队职责划分需真实生产环境,命令行不可复现(X)。",
"rows": [
{ "deliverable": "style-guide", "accountable": "art-director", "responsible": ["concept-artist"], "consulted": ["lead-artist", "tech-artist"], "informed": ["art-pm", "engineer"] },
{ "deliverable": "concept-art", "accountable": "lead-artist", "responsible": ["concept-artist"], "consulted": ["art-director"], "informed": ["art-pm"] },
{ "deliverable": "3d-model", "accountable": "lead-artist", "responsible": ["modeler"], "consulted": ["tech-artist"], "informed": ["art-pm"] },
{ "deliverable": "texture", "accountable": "lead-artist", "responsible": ["texture-artist"], "consulted": ["tech-artist"], "informed": ["art-pm"] },
{ "deliverable": "animation", "accountable": "lead-artist", "responsible": ["animator"], "consulted": ["tech-artist", "engineer"], "informed": ["art-pm"] },
{ "deliverable": "ui-kit", "accountable": "lead-artist", "responsible": ["ui-artist"], "consulted": ["art-director", "engineer"], "informed": ["art-pm"] },
{ "deliverable": "vfx", "accountable": "tech-artist", "responsible": ["vfx-artist"], "consulted": ["engineer"], "informed": ["art-pm"] },
{ "deliverable": "asset-manifest", "accountable": "tech-artist", "responsible": ["tech-artist"], "consulted": ["lead-artist"], "informed": ["art-pm", "engineer"] },
{ "deliverable": "outsource-batch", "accountable": "art-pm", "responsible": ["art-pm"], "consulted": ["lead-artist", "tech-artist"], "informed": ["art-director"] }
]
}
读这份配置有两个看点。第一,同一角色可以既是 A 又是 R——asset-manifest 与 outsource-batch 两行就是:TA 自己做清单、自己拍板收不收,这在「个人负责到底」的交付物上是合理的。第二,C 列表里绝不能出现该行的 A:如果 style-guide 的拍板人是 art-director,又把他列进自己的会签列表,等于「自己问自己」,这个约束下面会用测试锁死。
P 层:把矩阵编码成可断言的函数
下面这段是 P 层 TypeScript 内核 core/src/sp01/roles.ts。它把矩阵变成数据 + 三个查询/校验函数:accountableFor 查拍板人,checkRow 查单行冲突,conflicts 查全表。
// file: core/src/sp01/roles.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// 角色分工与职责矩阵:把「交付物」映射到唯一拍板角色(RACI 里的 A),并列出执行/会签/知会。
// 关键不变量:任何交付物只能有一个拍板角色(禁双主),且拍板角色不得出现在自己的会签列表里。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。
export type RoleId =
| "art-director" | "lead-artist" | "concept-artist" | "modeler"
| "texture-artist" | "animator" | "ui-artist" | "vfx-artist"
| "tech-artist" | "art-pm" | "engineer";
export const ROLE_IDS: RoleId[] = [
"art-director", "lead-artist", "concept-artist", "modeler", "texture-artist",
"animator", "ui-artist", "vfx-artist", "tech-artist", "art-pm", "engineer",
];
export type MatrixRow = {
deliverable: string;
accountable: RoleId; // A:最终拍板
responsible: RoleId[]; // R:执行
consulted: RoleId[]; // C:会签
informed: RoleId[]; // I:知会
};
export const MATRIX: MatrixRow[] = [
{ deliverable: "style-guide", accountable: "art-director", responsible: ["concept-artist"], consulted: ["lead-artist", "tech-artist"], informed: ["art-pm", "engineer"] },
{ deliverable: "concept-art", accountable: "lead-artist", responsible: ["concept-artist"], consulted: ["art-director"], informed: ["art-pm"] },
{ deliverable: "3d-model", accountable: "lead-artist", responsible: ["modeler"], consulted: ["tech-artist"], informed: ["art-pm"] },
{ deliverable: "texture", accountable: "lead-artist", responsible: ["texture-artist"], consulted: ["tech-artist"], informed: ["art-pm"] },
{ deliverable: "animation", accountable: "lead-artist", responsible: ["animator"], consulted: ["tech-artist", "engineer"], informed: ["art-pm"] },
{ deliverable: "ui-kit", accountable: "lead-artist", responsible: ["ui-artist"], consulted: ["art-director", "engineer"], informed: ["art-pm"] },
{ deliverable: "vfx", accountable: "tech-artist", responsible: ["vfx-artist"], consulted: ["engineer"], informed: ["art-pm"] },
{ deliverable: "asset-manifest", accountable: "tech-artist", responsible: ["tech-artist"], consulted: ["lead-artist"], informed: ["art-pm", "engineer"] },
{ deliverable: "outsource-batch", accountable: "art-pm", responsible: ["art-pm"], consulted: ["lead-artist", "tech-artist"], informed: ["art-director"] },
];
export function accountableFor(deliverable: string): RoleId | null {
const row = MATRIX.find((r) => r.deliverable === deliverable);
return row ? row.accountable : null;
}
/** 单行检查:返回该行的冲突(空数组即无冲突)。 */
export function checkRow(row: MatrixRow): string[] {
const out: string[] = [];
if (!ROLE_IDS.includes(row.accountable)) out.push(`${row.deliverable}: 未知拍板角色 ${row.accountable}`);
if (row.consulted.includes(row.accountable)) out.push(`${row.deliverable}: 拍板角色不应同时是会签`);
if (row.responsible.length === 0) out.push(`${row.deliverable}: 缺执行角色`);
return out;
}
/** 全表检查:逐行冲突 + 交付物名唯一。 */
export function conflicts(): string[] {
const out: string[] = [];
const seen = new Set<string>();
for (const row of MATRIX) {
if (seen.has(row.deliverable)) out.push(`${row.deliverable}: 交付物名重复`);
seen.add(row.deliverable);
out.push(...checkRow(row));
}
return out;
}
注意 MATRIX 与上面的 D 层配置是同一份数据的两种形态:配置给人和工具读,MATRIX 给类型系统读。二者一旦漂移,下一节的测试会立刻发现——这就是本套「一份规范、多处消费」在组织问题上的落法。
负例:双主必须被拦住
矩阵校验最怕的不是报错,是明明双主了却没人报。所以测试的力气全花在负例上。
// file: core/test/sp01_roles.test.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// 职责矩阵断言:D 层配置与 TS 内核逐行一致、全表无冲突、每个交付物有唯一拍板角色。
// 负例:把拍板角色塞进自己的会签列表,checkRow 必须抓到。
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { MATRIX, ROLE_IDS, accountableFor, checkRow, conflicts } from "../src/sp01/roles.ts";
import type { MatrixRow } from "../src/sp01/roles.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(HERE, "../..");
const cfg = JSON.parse(readFileSync(resolve(ROOT, "config/sp01_matrix.json"), "utf8"));
test("D 层矩阵配置与 TS 内核逐行一致(防漂移)", () => {
assert.equal(cfg.rows.length, MATRIX.length);
assert.deepEqual(MATRIX, cfg.rows);
});
test("职责矩阵全表无冲突", () => {
assert.deepEqual(conflicts(), []);
});
test("每个交付物有唯一拍板角色,且都在角色表内", () => {
const seen = new Set<string>();
for (const row of MATRIX) {
assert.equal(seen.has(row.deliverable), false, `${row.deliverable} 重复`);
seen.add(row.deliverable);
assert.ok(ROLE_IDS.includes(row.accountable), `${row.accountable} 不在角色表`);
}
assert.equal(accountableFor("style-guide"), "art-director");
assert.equal(accountableFor("asset-manifest"), "tech-artist");
assert.equal(accountableFor("不存在的交付物"), null);
});
test("风格与规格的拍板权不在同一角色(避免双主)", () => {
assert.notEqual(accountableFor("style-guide"), accountableFor("asset-manifest"));
});
test("负例:拍板角色同时出现在会签列表必须被抓到", () => {
const bad: MatrixRow = {
deliverable: "style-guide",
accountable: "art-director",
responsible: ["concept-artist"],
consulted: ["lead-artist", "art-director"],
informed: ["art-pm"],
};
const errs = checkRow(bad);
assert.ok(errs.length > 0, "预期报冲突,实际无");
assert.ok(errs.some((e) => e.includes("会签")), errs.join("|"));
});
test("负例:未知拍板角色必须被抓到", () => {
const bad: MatrixRow = {
deliverable: "x",
accountable: "not-a-role" as never,
responsible: ["modeler"],
consulted: [],
informed: [],
};
assert.ok(checkRow(bad).some((e) => e.includes("未知拍板角色")));
});
在本机跑 node --test 'core/test/sp01_roles.test.ts',真实输出:
✔ D 层矩阵配置与 TS 内核逐行一致(防漂移) (1.077583ms)
✔ 职责矩阵全表无冲突 (0.132917ms)
✔ 每个交付物有唯一拍板角色,且都在角色表内 (0.157042ms)
✔ 风格与规格的拍板权不在同一角色(避免双主) (0.073958ms)
✔ 负例:拍板角色同时出现在会签列表必须被抓到 (0.818916ms)
✔ 负例:未知拍板角色必须被抓到 (0.580125ms)
ℹ tests 6
ℹ pass 6
ℹ fail 0
两条负例分别守住「自己不能问自己」与「不能把拍板权交给不存在的人」。这两条一旦从失败变成通过,说明约束被删掉了,而不是矩阵变干净了。
常见分工事故
把矩阵落地时,下面四种事故几乎每个团队都会撞上一次:
| 事故 | 症状 | 矩阵层面的修法 |
|---|---|---|
| 双主 | 两个人都说自己拍板 | 交付物拆开,各占一行,A 各自唯一 |
| 无人拍板 | 大家都说「我不管这个」 | 补齐该行的 A,宁可由 PM 代持 |
| 会签变审批 | 拍板前要过五个人,排期无限后延 | C 只保留真能一票否决的角色 |
| 执行缺位 | 有 A 有 C,没有 R | 补 R,否则判定通过也没人做 |
最隐蔽的是第三种:把「知情者」当「会签者」。I 是事后告知,C 是拍板前必问。把太多人塞进 C,等于把决策权稀释成审批链——这是大团队美术效率塌陷的头号原因。
自检表
| 检查项 | 合格标准 |
|---|---|
| 每行有 A | 每件交付物的最终拍板角色唯一 |
| A 不在 C | 拍板角色不出现于自己的会签列表 |
| 每行有 R | 至少一个执行角色,判定通过后有人做 |
| 角色封闭 | 矩阵里出现的角色都在角色表内 |
| 配置与代码一致 | D 层配置与 TS 内核逐行一致,漂移即失败 |
| 边界清晰 | 真实团队职责划分标 X,不写成「实测」 |
角色定清楚了,还剩一个位置最难摆:技术美术。他既不是纯美术,也不是纯程序,却同时握着「规格拍板」与「会签接入」两件事。下一节专门讲 TA 站在桥上的哪个位置,以及一套把「什么归本套讲、什么外链出去」写成可断言路由表的方法。
阅读导航:上一节:1.1 美术生产链路:概念 → 制作 → 接入 → 验收 · 下一节:1.3 技术美术的位置:桥接美术与程序(含 ART_SCOPE 分工表) 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。