前两节把链路和角色都定了。但有一个位置始终最难摆:技术美术(TA)。他既不是纯美术——不靠画得好看取胜;也不是纯程序——不负责引擎实现。他站在两者中间,做的是把「好看」翻译成「能跑且不超预算」。这一节讲清他站的位置,并给出一张 ART_SCOPE 分工表:一张把「什么归本套讲、什么外链出去」写成可断言路由表的表。
技术美术到底做什么:三类工作
TA 的职责很容易被含糊成「美术和程序之间的翻译」。含糊的代价是:谁都能说自己在做 TA,谁也说不出 TA 交付了什么。把它切成三类可交付的工作,边界就清楚了:
| 类别 | 解决什么 | 典型产出物 |
|---|---|---|
| 规格与预算 | 把风格目标落成可测参数 | 预算表、材质参数表、LOD 规范 |
| 管线与工具 | 让资产从 DCC 顺畅进引擎 | 导入校验、图集/纹理管线、依赖检查 |
| 桥接与裁决 | 在美术与程序之间做技术裁决 | 可行性结论、接入规范、争议记录 |
三类里最容易被低估的是第三类。「可行性裁决」不是开会表态,而是给出可验证的结论:这套特效在低端机上能不能保住帧率、这个面数预算会不会让一批资产全部返工。裁决一旦含糊,美术与程序就会各自按对自己有利的口径理解,最后在集成期爆炸。
桥上的三个接口
TA 站在桥上,脚下是三个接口。每个接口都有明确的输入、输出与谁拍板:
| 接口 | 上游 | 下游 | TA 的角色 |
|---|---|---|---|
| 规格接口 | 美术总监的风格目标 | 全体美术 | 把目标转成可测参数并拍板规格 |
| 管线接口 | 画师的产出 | 程序/引擎 | 定义导入规范并会签接入 |
| 裁决接口 | 美术诉求 | 程序约束 | 给可行性结论,不作最终风格拍板 |
第三个接口是分寸感的关键:TA 拍规格,不拍风格。风格最终拍板在美术总监(见上一节职责矩阵),TA 只对「这套风格在技术约束下能不能成立」负责。把这两件事混在一起,TA 就会从桥变成墙。
ART_SCOPE 分工表:什么归本套,什么外链出去
写一本讲美术规范的书,最大的风险是什么都想讲,结果什么都讲不深。所以本套在动笔前先立了一张 ART_SCOPE 分工表,把主题域分成两类:this-book(本套自己讲)与 external(外链既有内容,本套不重推)。
| 主题域 | 归属 | 外链目标 |
|---|---|---|
| 渲染原理 / 着色器编译 / 纹理压缩格式 / 骨骼动画数学 / 后处理 / 色彩管理 HDR | external | /posts/graphics/ 及 graphics 各专题 |
| 引擎实现 / RHI / 帧图 / 剔除 LOD / GPU 粒子 / 合批 | external | 引擎卷与 graphics |
| 美术风格指南短文 / 性能预算短文 / 外包短文 | external | game/industry/* |
| 调色板 / 命名 / 预算 / 图集 / 管线 / 风格谱系 / 中台 / 工业化 | this-book | 本套 |
这张表的价值不在「分得对」,而在它可以被机器检查。下面这段是 P 层 TypeScript 内核 core/src/sp01/art_scope.ts,把表编码成一张路由表:给定主题,返回 this-book 或 external 加外链路径。
// file: core/src/sp01/art_scope.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// ART_SCOPE 分工路由表:把「主题域」判给 this-book(本套自己讲)或 external(外链既有文章)。
// 编码成可断言的路由表 + 负例测试。external 路径全部取自 BRIEF §9.3 已逐个核验的清单。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。
export type Owner = "this-book" | "external";
export type ScopeRule = {
id: string;
owner: Owner;
match: string[];
path: string | null;
};
// §9.3 已核验存在的绝对 URL(external 规则只能取这里的值)。
export const VERIFIED_URLS: string[] = [
"/posts/graphics/",
"/graphics-color-management-hdr-pipeline/",
"/graphics-shader-compilation-pipeline/",
"/graphics-texture-memory/",
"/graphics-skeletal-animation/",
"/graphics-post-processing/",
"/graphics-gpu-particles-vfx/",
"/graphics-mobile-rendering-optimization/",
"/graphics-gpu-driven-rendering-lod/",
"/graphics-render-graph/",
"/helio-render-01-3-scope-vs-graphics/",
"/game-art-direction-style-bible-2026/",
"/game-performance-budget-mobile-pc-2026/",
"/posts/game/industry/",
];
export const ART_SCOPE: ScopeRule[] = [
// ---- external:渲染原理(graphics 专题 45 篇,是「原理的家」)----
{ id: "render-principle", owner: "external", path: "/posts/graphics/", match: ["渲染原理", "渲染管线原理", "光照原理", "着色模型"] },
{ id: "shader-compile", owner: "external", path: "/graphics-shader-compilation-pipeline/", match: ["着色器编译", "shader 编译"] },
{ id: "texture-format", owner: "external", path: "/graphics-texture-memory/", match: ["纹理压缩格式", "纹理显存"] },
{ id: "skinning-math", owner: "external", path: "/graphics-skeletal-animation/", match: ["骨骼动画数学", "蒙皮数学"] },
{ id: "post-process", owner: "external", path: "/graphics-post-processing/", match: ["后处理"] },
{ id: "color-hdr", owner: "external", path: "/graphics-color-management-hdr-pipeline/", match: ["色彩管理", "HDR"] },
// ---- external:引擎实现(《渲染引擎》《引擎内核》卷)----
{ id: "engine-impl", owner: "external", path: "/helio-render-01-3-scope-vs-graphics/", match: ["引擎实现", "RHI"] },
{ id: "render-graph", owner: "external", path: "/graphics-render-graph/", match: ["帧图", "render graph"] },
{ id: "cull-lod", owner: "external", path: "/graphics-gpu-driven-rendering-lod/", match: ["剔除 LOD", "GPU 剔除"] },
{ id: "gpu-particle", owner: "external", path: "/graphics-gpu-particles-vfx/", match: ["GPU 粒子"] },
{ id: "batching", owner: "external", path: "/graphics-mobile-rendering-optimization/", match: ["合批", "批处理"] },
// ---- external:撞车短文(§9.2)----
{ id: "style-bible-essay", owner: "external", path: "/game-art-direction-style-bible-2026/", match: ["美术风格指南短文", "风格指南短文"] },
{ id: "perf-budget-essay", owner: "external", path: "/game-performance-budget-mobile-pc-2026/", match: ["性能预算短文"] },
{ id: "outsourcing-essay", owner: "external", path: "/posts/game/industry/", match: ["外包短文"] },
// ---- this-book:本套自己的地盘 ----
{ id: "palette", owner: "this-book", path: null, match: ["调色板", "配色"] },
{ id: "naming", owner: "this-book", path: null, match: ["命名", "资产命名"] },
{ id: "art-budget", owner: "this-book", path: null, match: ["美术预算", "面数预算", "骨骼预算", "材质槽预算", "预算"] },
{ id: "atlas", owner: "this-book", path: null, match: ["图集"] },
{ id: "pipeline", owner: "this-book", path: null, match: ["资产管线", "生产链路", "管线"] },
{ id: "style-gen", owner: "this-book", path: null, match: ["风格谱系", "风格向量", "风格距离"] },
{ id: "platform", owner: "this-book", path: null, match: ["中台"] },
{ id: "industrialization", owner: "this-book", path: null, match: ["工业化"] },
];
export type RouteResult = { owner: Owner; path: string | null; ruleId: string };
/** 首个命中的规则胜出(故 external 具体规则必须排在 this-book 泛化规则之前)。 */
export function route(topic: string): RouteResult | null {
for (const rule of ART_SCOPE) {
if (rule.match.some((k) => topic.includes(k))) {
return { owner: rule.owner, path: rule.path, ruleId: rule.id };
}
}
return null;
}
export function externalRules(): ScopeRule[] {
return ART_SCOPE.filter((r) => r.owner === "external");
}
export function externalPaths(): string[] {
return externalRules().map((r) => r.path as string);
}
规则顺序不是随便排的:external 的具体规则必须排在 this-book 的泛化规则之前。因为 route 取首个命中,「着色器编译管线」里既有 着色器编译 也有 管线——如果不把 shader-compile 排在 pipeline 前面,它就会被本套的泛化规则「抢走」,判成 this-book。这个坑下面有专门的负例锁死。
负例:渲染原理绝不能被本套收编
一张分工表最危险的失败,是越界:把本该外链的原理性内容收进本套,写成一篇四不像的浅文。测试的力气因此全花在负例上。
// file: core/test/sp01_art_scope.test.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// ART_SCOPE 路由表断言:external 路径两两不同、全部在 §9.3 已核验清单内、this-book 规则不带外链。
// 负例是核心 —— 「渲染原理」必须判给 external;朴素路由器把它判成 this-book,必须被拦住。
import test from "node:test";
import assert from "node:assert/strict";
import {
ART_SCOPE,
VERIFIED_URLS,
route,
externalRules,
externalPaths,
} from "../src/sp01/art_scope.ts";
import type { Owner } from "../src/sp01/art_scope.ts";
test("规则 id 唯一、match 非空", () => {
const ids = ART_SCOPE.map((r) => r.id);
assert.equal(new Set(ids).size, ids.length, "规则 id 重复");
for (const r of ART_SCOPE) assert.ok(r.match.length > 0, `${r.id} 无匹配词`);
});
test("不变量:external 路径两两不同、条数等于去重条数、全部在已核验清单内", () => {
const paths = externalPaths();
assert.equal(paths.length, new Set(paths).size, "external 路径有重复");
assert.equal(externalRules().length, paths.length, "external 规则数与路径数不符");
const verified = new Set(VERIFIED_URLS);
for (const p of paths) assert.ok(verified.has(p), `${p} 不在 §9.3 已核验清单`);
});
test("this-book 规则不带外链路径", () => {
for (const r of ART_SCOPE) {
if (r.owner === "this-book") assert.equal(r.path, null, `${r.id} 不应带 path`);
}
});
test("external 规则必须带 path", () => {
for (const r of externalRules()) assert.ok(r.path, `${r.id} 缺 path`);
});
test("正例:本套地盘判给 this-book", () => {
for (const t of ["调色板", "资产命名", "美术预算", "图集", "资产管线", "风格谱系", "美术中台", "美术工业化"]) {
const r = route(t);
assert.ok(r, `${t} 未命中`);
assert.equal(r.owner, "this-book", `${t} 应判 this-book,实得 ${r.owner}`);
}
});
test("正例:原理与实现判给 external", () => {
assert.equal(route("渲染原理")?.owner, "external");
assert.equal(route("着色器编译")?.owner, "external");
assert.equal(route("骨骼动画数学")?.owner, "external");
assert.equal(route("引擎实现")?.owner, "external");
assert.equal(route("合批")?.owner, "external");
assert.equal(route("色彩管理")?.owner, "external");
});
test("负例:朴素路由器把渲染原理判成 this-book,真路由表必须不这么判", () => {
// 朴素路由器:见到「原理」「管线」就收进本套 —— 这正是 §9.1 要防的越界
const naive = (t: string): Owner => (t.includes("原理") || t.includes("管线") ? "this-book" : "external");
assert.equal(naive("渲染原理"), "this-book"); // 朴素路由器确实会错判
const r = route("渲染原理");
assert.ok(r);
assert.notEqual(r.owner, "this-book");
assert.equal(r.owner, "external");
assert.equal(r.path, "/posts/graphics/");
});
test("负例:着色器编译含「管线」二字,但不得被本套的管线规则抢走", () => {
const r = route("着色器编译管线");
assert.ok(r);
assert.equal(r.owner, "external");
assert.equal(r.ruleId, "shader-compile");
});
test("未覆盖主题返回 null(需要新增规则,而不是静默归到本套)", () => {
assert.equal(route("完全没听过的主题域"), null);
});
在本机跑 node --test 'core/test/sp01_art_scope.test.ts',真实输出:
✔ 规则 id 唯一、match 非空 (0.917625ms)
✔ 不变量:external 路径两两不同、条数等于去重条数、全部在已核验清单内 (0.157375ms)
✔ this-book 规则不带外链路径 (0.081125ms)
✔ external 规则必须带 path (0.069125ms)
✔ 正例:本套地盘判给 this-book (0.685584ms)
✔ 正例:原理与实现判给 external (0.160208ms)
✔ 负例:朴素路由器把渲染原理判成 this-book,真路由表必须不这么判 (0.274042ms)
✔ 负例:着色器编译含「管线」二字,但不得被本套的管线规则抢走 (0.080208ms)
✔ 未覆盖主题返回 null(需要新增规则,而不是静默归到本套) (0.092125ms)
ℹ tests 9
ℹ pass 9
ℹ fail 0
两条负例守的是两个方向的越界:一条防「把原理收进本套」,一条防「泛化规则抢走具体规则」。只要这两条里的任何一条从「拦住」变成「放行」,分工表就已经失效了。
怎么用这张表
ART_SCOPE 不是写完就归档的文档,它在三个地方被反复消费:
- 动笔前:新开一节先
route一下主题,判成external的就只写一句外链,不展开。 - 写作中:正文里出现越界论述,说明路由表该补规则了——补规则比补一段浅文便宜得多。
- 验收时:把本节标题喂进
route,this-book的才允许出现在本套目录里。
这也解释了本套为什么敢写「50 篇 × 3 卷」这么大的体量:边界不是靠自律守住的,是靠一张可断言的表守住的。
自检表
| 检查项 | 合格标准 |
|---|---|
| 三类工作清楚 | 规格 / 管线 / 桥接各有产出物 |
| 接口有拍板 | 三个接口各自写明谁拍板 |
| 分工表可断言 | ART_SCOPE 是可跑的路由表,不是文档 |
| 越界有负例 | 「渲染原理判成 this-book」有测试拦住 |
| 路径可核验 | external 路径全部来自已核验清单 |
| 边界清晰 | 真实 DCC 与渲染实现外链,本套不重推 |
TA 站的是桥,不是墙:他把「好看」翻译成「能跑且不超预算」,但不替美术总监决定什么算好看。第 1 章到此收尾——链路、角色、分工三件事都立住了。下一章我们进入本套的第一块硬骨头:怎么把「清新治愈」这种形容词,翻译成一组可测参数。
阅读导航:上一节:1.2 角色分工与职责矩阵 · 下一节:2.1 从「治愈」「硬核」到可测参数 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。