10.3 X:画质

前两节把二次元里能写成数据的部分写完了:色彩、线色、色阶带、头身比、面部特征线。剩下的那一半——画质——本机根本做不了。本节整节标 X,逐条说明它为什么不可复现,并把「不许给不可复现的东西编一个数字」写成一条会失败的门禁:声明表里只要出现一个测量值字段,测试立刻报错。

10.1 与 10.2 把二次元里能写成数据的部分写完了:角色色、线色约束、色阶带、头身比、面部特征线。剩下的那一半——画质本身好不好——本机根本做不了。这一节整节标 X:正文里每一处涉及画质的判断,都必须显式标注「需真实环境(真实画师 / 真实绘图板 / 真实环境),命令行不可复现」。站内 2D 手绘与矢量 那一节已经把「画质与笔触」归到 X,本节接着它往下走:不只承认不可复现,而是把不可复现本身编码成一条会失败的门禁。

画质为什么不可复现

先把「不可复现」说清楚,它不是一句偷懒的话。本机没有绘图板、没有 Photoshop / Krita / Clip Studio,也不可能安装。即使有,画质也不是「跑一遍就得到同一个数」的东西:

画质的维度为什么不可复现本机的处理
线稿的粗细变化(起收笔、转角加粗)依赖手的压力曲线与运笔速度X,需真实环境,命令行不可复现
眼睛的高光与瞳孔层次依赖画师对「眼神」的判断X,需真实环境,命令行不可复现
发丝的分组与前后穿插依赖画师对体积与剪影的理解X,需真实环境,命令行不可复现
赛璐璐阴影的边界是否干净依赖逐块填色的手感与边缘处理X,需真实环境,命令行不可复现
上色是否发脏(中间调是否被压成灰)依赖画师的眼睛(色域参数只是边界)X,需真实环境,命令行不可复现
缩到游戏尺寸后是否仍可辨识依赖真实分辨率下的观看X,需真实环境,命令行不可复现

右列每一行都写着同一句话,这不是重复,是纪律:任何一行漏标,就会被误读成「本机能算」。而这套书稿最想拦住的事,恰恰是有人给「不可复现」的东西编一个看起来很像真的数字——比如「画风相似度 0.92」「可爱度 8.7/10」「线稿自然度 87%」。

这类数字的危险不在它错,而在它无从证伪。一个面部特征线间距 30 < 下限 40,任何人重跑一遍都能复现这个结论;但「画风相似度 0.92」没有任何人能复现,它只会让评审会陷入「我觉得是 0.95」的扯皮。不可复现的量一旦被赋予小数,就失去了被检验的可能。

X 层:画质验收声明表

下面这段是 X 层声明表 config/st10_quality_claims.json。它的字段结构可以静态校验,但每一条的 criterion 真值来自真实画师,命令行不可复现。关键在于 forbiddenFields:它明确列出「测量值」类字段名,任何人往里加一个 measuredValue 都会被下一节的门禁拦下:

{
  "version": 1,
  "project": "stardust-inn",
  "tier": "X",
  "note": "二次元 / 日式的画质验收声明表(第 10 章 10.3,X 层)。画质(线稿的粗细变化、眼睛高光与瞳孔层次、发丝分组与穿插、赛璐璐阴影边界、上色是否发脏、缩到游戏尺寸后是否可辨识)只能由真实画师在真实环境里判断,命令行不可复现。本表把这些验收项写成可静态校验的声明:每条只允许 evidence=\"requires-real-env\",且**禁止携带任何测量值字段**(measuredValue / metricValue / score / confidence)——一旦出现,就说明有人给不可复现的东西编了一个数字。",
  "allowedEvidence": ["requires-real-env"],
  "forbiddenFields": ["measuredValue", "metricValue", "score", "confidence"],
  "claims": [
    { "id": "cq_line-taper", "criterion": "线稿的粗细变化是否自然(起收笔、转角加粗)", "judge": "画师主观评审", "evidence": "requires-real-env" },
    { "id": "cq_eye-highlight", "criterion": "眼睛的高光与瞳孔层次是否成立", "judge": "画师主观评审", "evidence": "requires-real-env" },
    { "id": "cq_hair-grouping", "criterion": "发丝的分组与前后穿插是否读得出来", "judge": "主美评审", "evidence": "requires-real-env" },
    { "id": "cq_cel-edge", "criterion": "赛璐璐阴影的边界是否干净、无锯齿与脏边", "judge": "画师主观评审", "evidence": "requires-real-env" },
    { "id": "cq_color-dirty", "criterion": "上色是否发脏(中间调是否被压成灰)", "judge": "美术总监评审", "evidence": "requires-real-env" },
    { "id": "cq_small-size-read", "criterion": "缩到游戏实际尺寸后角色剪影与表情是否仍可辨识", "judge": "主美评审", "evidence": "requires-real-env" }
  ]
}

evidence 这个字段本身就是被校验的对象:它只允许 requires-real-env。「本机做不了」在这里不是注释,是枚举值。

P 层:把「不许编数字」写成门禁

下面这段是 P 层内核 core/src/st10/quality_claim.ts。它一行都不评价画质——它只做一件事:扫描整张声明表,任何一条若声称可命令行复现,或携带任何被禁的「测量值」字段,即判违规:

// file: core/src/st10/quality_claim.ts
// tier: P
// verify: node --test 'core/test/st10_*.test.ts'
// 二次元画质声明表的静态校验内核(P 层)。它**不评价画质**,只做一件事:把「不可复现的
// 验收项」写成会失败的门禁——任何一条声明若声称可命令行复现(evidence 不是
// requires-real-env),或携带任何「测量值」字段(measuredValue / score…),即判违规。
// 这正是本套「不假装能真画」的纪律:不是留白,而是把留白本身编码成断言。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。

export type Claim = { id: string; criterion: string; judge: string; evidence: string };
export type ClaimSpec = {
  allowedEvidence: string[];
  forbiddenFields: string[];
  claims: (Claim & Record<string, unknown>)[];
};

export type ClaimReport = {
  claimCount: number;
  violations: string[];
  inventedCount: number;
  realEnvCount: number;
  pass: boolean;
};

const ID_PATTERN = /^cq_[a-z][a-z0-9-]*$/;

/** 校验整张声明表:结构 + 诚实性。 */
export function validateClaims(spec: ClaimSpec): ClaimReport {
  const violations: string[] = [];
  let inventedCount = 0;
  let realEnvCount = 0;
  for (const c of spec.claims) {
    if (!ID_PATTERN.test(String(c.id))) violations.push(`${c.id}: id 不合文法`);
    if (!c.criterion) violations.push(`${c.id}: 缺验收描述`);
    if (!c.judge) violations.push(`${c.id}: 缺评审角色`);
    if (!spec.allowedEvidence.includes(String(c.evidence))) {
      violations.push(`${c.id}: evidence=${c.evidence} 越界,仅允许 ${spec.allowedEvidence.join("/")}`);
    } else {
      realEnvCount += 1;
    }
    for (const f of spec.forbiddenFields) {
      if (f in c) {
        violations.push(`${c.id}: 出现被禁字段 ${f}`);
        inventedCount += 1;
      }
    }
  }
  return {
    claimCount: spec.claims.length, violations, inventedCount, realEnvCount,
    pass: violations.length === 0 && inventedCount === 0,
  };
}

注意 pass 的判据:violations.length === 0 && inventedCount === 0。只要有一条声明带了测量值,pass 就是假——这不是「警告」,是硬失败。

负例:编出来的数字必须被拦下

这个门禁最怕的失败模式是放行:明明有人编了一个数,却没人报。所以测试的力气全在负例上:

// file: core/test/st10_quality_claim.test.ts
// tier: P
// verify: node --test 'core/test/st10_*.test.ts'
// 画质声明表的断言:基线 6 条声明全部标 requires-real-env、零违例、零编造;
// 负例:声称可命令行复现、携带测量值字段、id 不合文法,都必须被判出来。
// 这层校验不产生任何画质数字——它只保证「没有人给不可复现的东西编数字」。
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { validateClaims } from "../src/st10/quality_claim.ts";
import type { ClaimSpec } from "../src/st10/quality_claim.ts";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const spec = JSON.parse(readFileSync(resolve(ROOT, "config/st10_quality_claims.json"), "utf8")) as ClaimSpec;
const sha16 = (s: string): string => createHash("sha256").update(s, "utf8").digest("hex").slice(0, 16);

test("基线:6 条声明全部标 requires-real-env,零违例、零编造", () => {
  const r = validateClaims(spec);
  assert.equal(r.claimCount, 6);
  assert.deepEqual(r.violations, []);
  assert.equal(r.inventedCount, 0);
  assert.equal(r.realEnvCount, 6);
  assert.equal(r.pass, true);
});

test("负例:声称可命令行复现(evidence=cli)必须被判违规", () => {
  const bad: ClaimSpec = { ...spec, claims: [{ ...spec.claims[0], evidence: "cli" }] };
  const r = validateClaims(bad);
  assert.equal(r.pass, false);
  assert.ok(r.violations.some((e) => /evidence=cli/.test(e)));
  assert.equal(r.realEnvCount, 0);
});

test("负例:给不可复现的项编一个测量值,必须被拦下", () => {
  const bad: ClaimSpec = {
    ...spec,
    claims: [{ ...spec.claims[0], measuredValue: 0.87 } as ClaimSpec["claims"][number]],
  };
  const r = validateClaims(bad);
  assert.equal(r.inventedCount, 1);
  assert.ok(r.violations.some((e) => /被禁字段 measuredValue/.test(e)));
  assert.equal(r.pass, false);
});

test("负例:id 不合文法、缺评审角色都要被抓", () => {
  const bad: ClaimSpec = { ...spec, claims: [{ ...spec.claims[0], id: "claim_bad", judge: "" }] };
  const r = validateClaims(bad);
  assert.ok(r.violations.some((e) => /id 不合文法/.test(e)));
  assert.ok(r.violations.some((e) => /缺评审角色/.test(e)));
});

test("P 层摘要(digest):本卷无 S 层,故只有诚实性自洽摘要", () => {
  const r = validateClaims(spec);
  const canon = JSON.stringify({
    claimCount: r.claimCount, realEnvCount: r.realEnvCount, inventedCount: r.inventedCount,
    pass: r.pass, ids: spec.claims.map((c) => c.id),
  });
  assert.equal(sha16(canon), "bdd3af306b5006d9");
  console.log(`st10 quality/claim digest = ${sha16(canon)}`);
});

在本机跑 node --test 'core/test/st10_*.test.ts',本节 5 条(三节合计 15 条),真实输出:

st10 quality/claim digest = bdd3af306b5006d9
✔ 基线:6 条声明全部标 requires-real-env,零违例、零编造 (0.936542ms)
✔ 负例:声称可命令行复现(evidence=cli)必须被判违规 (0.133042ms)
✔ 负例:给不可复现的项编一个测量值,必须被拦下 (0.082125ms)
✔ 负例:id 不合文法、缺评审角色都要被抓 (0.08325ms)
✔ P 层摘要(digest):本卷无 S 层,故只有诚实性自洽摘要 (0.674917ms)
ℹ tests 5
ℹ pass 5
ℹ fail 0

那条 measuredValue: 0.87 的负例是整个第 10 章最想讲的一行:它不是「测出来的 0.87」,是有人凭空写的 0.87。门禁拦的不是这个数字的大小,而是「这个字段不该存在」这件事本身。

这一章的三层分工

回到全章。二次元是这套书稿里第二个没有 S 层的一章(第一个是 7.3 X:笔触质量 所在的厚涂章),它的三层是这样分的:

层管什么产物可复现性
D色彩 / 线色 / 色阶带 / 头身比 / 面部比例两份 JSON静态可校验
P把规范变成会失败的断言三个 TS 内核 + 测试node --test,15 条
X画质本身声明表需真实环境,命令行不可复现

这套分法的要点是:X 不是留白,是被显式编码的边界。一个团队如果只写 D 与 P,规范会给人「什么都验过了」的错觉;把 X 显式列出来,评审会上就不会有人问「画质那个指标呢」——因为答案是明确的:它没有指标,它只有画师的眼睛。

真实环境里怎么验收画质

既然命令行不可复现,验收就只能在真实环境里做。下面这套流程本机一步都跑不了,写出来是为了让「X」这个标签有具体所指,而不是一句免责声明:

  1. 真实绘图板与真实笔刷:在 Photoshop / Krita / Clip Studio 里按 10.1 的线色约束与色阶带画一遍线稿与赛璐璐上色。需真实环境,命令行不可复现。
  2. 真实分辨率观看:把画好的角色缩到游戏实际显示的尺寸(如 96×128)再看一遍剪影与表情。需真实环境,命令行不可复现。
  3. 真实画师评审:由画师与主美按 cq_line-taper … cq_small-size-read 六条逐项过一遍,给出「过 / 打回」。需真实环境,命令行不可复现。
  4. 真实引擎内嵌:把角色挂进真实 UI 或场景里,在真实光照与真实 UI 底色下看一遍。需真实环境,命令行不可复现。

四步的共同点是:没有一步能由 CI 自动判定。这也是本节把 evidence 锁成 requires-real-env 的原因——不是不想自动化,是这些判断本质上依赖人的感知,自动化只会得到一个编出来的数。能给自动化的是前两节的 D 与 P:角色色有没有越界、色阶带有没有塌成一档、头身比自不自洽、面部顺序有没有错。把能自动化的全部自动化,把不能自动化的显式标出来,这才是一份诚实的规范。

自检表

检查项合格标准
X 显式每处涉及画质的判断都标注「需真实环境,命令行不可复现」
声明可判每条声明都有 id、验收描述、评审角色、evidence
诚实性硬失败出现任一测量值字段,pass 即为假
不编数字全文没有任何画质的「测量值」
边界清晰画质只作主观评审,不写成「实测」

二次元收尾了。下一章换一种完全不同的情绪:暗黑 / 恐怖。它和二次元正好相反——二次元靠高明度与干净线条,暗黑则几乎全靠低饱和与亮度分布,而这两样恰恰都能算成整数账。

阅读导航:上一节:10.2 角色比例与面部规范 · 下一节:11.1 低饱和色彩分级 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「gameart」更多文章

  1. 附录 A:风格参数表、量化规则与数据集索引
  2. 9.3 等距资产的制作约束
  3. 9.2 等距 tile 与遮挡规则