6.3 划界:编译与渲染原理外链

本节不是内容而是边界:第 6 章只讲参数表与默认值、参数对齐与打包,着色器编译、变体爆炸、渲染管线原理一律外链给 graphics 专题。把「什么归本章讲」编码成一张可断言的路由表——external 规则在前、this-book 在后,首个命中胜出;三条外链路径两两不同、全部在 public/ 产物中存在,越界与漏网都被断言挡住。

第 6 章只讲两件事:参数表与默认值(6.1)、参数对齐与打包(6.2)。剩下的——着色器怎么编译、一次编译出多少变体、渲染管线怎么组织——本卷一个字都不讲,一律外链给 graphics 专题。这一节不是「内容」,而是边界:把「什么归本章讲」编码成一张可断言的路由表,越界就报警。

6.3 划界:编译与渲染原理外链

6.3.1 什么归本章讲,什么一律外链

为什么要把边界写成代码?因为越界是默认行为。一个朴素的路由器看到「着色器编译」里有「编译」两个字,就会把它收进本卷——这正是撞车的开始。划界要反过来做:先把原理主题全部标成 external,且排在 this-book 规则之前,首个命中的规则胜出;只有真正属于本章的主题才判给 this-book。

判不出来的主题返回 null,逼你去补规则,而不是静默归到本章。这张表越严,本卷与 graphics 专题的分工越清楚。

6.3.2 外链清单:三个原理之家

本章涉及但不展开的三个原理主题,各自外链到一篇既有文章:

主题域归属外链目标
着色器编译 / 编译产物 / 编译期external着色器编译与 SPIR-V 工具链
shader 变体爆炸 / 关键字变体externalMesh Shader 新一代几何管线
渲染管线原理 / 帧图 / 管线阶段external渲染图与帧调度架构

这三篇是原理的家:讲清楚「为什么」,本卷只讲「美术侧怎么落成规范与账」。要原理,去那三篇;要参数表、对齐与打包,留在本章。

6.3.3 划界路由表(D 层)

路由表本身是一份 D 层数据:规则按顺序排列,external 在前、this-book 在后;verifiedUrls 是已核验的绝对 URL 清单:

{
  "project": "stardust-inn",
  "ruleSet": "chapter-scope",
  "specRef": "config/visual_spec.json",
  "note": "第 6 章划界路由表:把「什么归本章讲(this-book)、什么一律外链(external)」编码成可断言的数据。external 规则必须排在 this-book 规则之前(首个命中胜出)。三条 external 规则的路径两两不同、全部取自已核验清单,且都在 public/ 产物中存在。",
  "verifiedUrls": [
    "/graphics-shader-compilation-pipeline/",
    "/graphics-mesh-shaders-nextgen/",
    "/graphics-render-graph/",
    "/posts/graphics/"
  ],
  "probes": [
    "着色器编译",
    "shader 变体爆炸",
    "渲染管线原理",
    "材质参数表",
    "参数对齐与打包",
    "材质槽预算",
    "完全没听过的主题"
  ],
  "rules": [
    { "id": "shader-compile", "owner": "external", "path": "/graphics-shader-compilation-pipeline/", "match": ["着色器编译", "shader 编译", "编译产物", "编译期"] },
    { "id": "shader-variant", "owner": "external", "path": "/graphics-mesh-shaders-nextgen/", "match": ["变体爆炸", "shader 变体", "关键字变体", "permutation"] },
    { "id": "render-pipeline", "owner": "external", "path": "/graphics-render-graph/", "match": ["渲染管线原理", "管线阶段", "帧图", "render graph"] },
    { "id": "material-param-table", "owner": "this-book", "path": null, "match": ["材质参数表", "参数表", "默认值", "参数契约"] },
    { "id": "param-packing", "owner": "this-book", "path": null, "match": ["参数对齐", "参数打包", "寄存器排布", "常量缓冲"] },
    { "id": "material-slot-budget", "owner": "this-book", "path": null, "match": ["材质槽", "材质预算"] }
  ]
}

三条 external 规则的路径两两不同、且全部取自已核验清单——这正是 §9.1 的不变量:external 规则条数 == 既有文章条数。

6.3.4 S 层:路由与产物核对

S 层脚本对一串探针主题逐个路由,并逐条核对 external 路径在 public/ 产物中真实存在:

#!/usr/bin/env python3
# file: sim/ta06_scope.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta06_scope.py
"""6.3 第 6 章划界路由表(S 层)。本章只讲材质参数表与默认值、参数对齐与打包;着色器编译、
变体爆炸、渲染管线原理一律外链,本卷不重推。本脚本把这张划界表跑成可断言的账:对一串探针主题
逐个路由落 out/ta06_scope.json;断言 external 规则数 == 去重后的 external 路径数(路径两两不同);
逐条核对每个 external 路径在仓库 public/ 产物中真实存在(页面查 index.html)。
"""
import hashlib
import json
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPO = "/Users/bingrong.yan/projects/birdor/plumephp"
PUBLIC = os.path.join(REPO, "public")
CONFIG = os.path.join(ROOT, "config/ta06_scope.json")
OUT = os.path.join(ROOT, "out")


def route(rules, topic):
    """首个命中的规则胜出。"""
    for r in rules:
        if any(k in topic for k in r["match"]):
            return {"topic": topic, "owner": r["owner"], "path": r["path"], "ruleId": r["id"]}
    return {"topic": topic, "owner": None, "path": None, "ruleId": None}


def path_exists(url):
    """页面目标查 index.html;带扩展名的目标查文件本身。"""
    tail = url.rstrip("/").split("/")[-1]
    if "." in tail:
        return os.path.isfile(os.path.join(PUBLIC, url.lstrip("/")))
    return os.path.exists(os.path.join(PUBLIC, url.strip("/"), "index.html"))


def main():
    cfg = json.load(open(CONFIG, encoding="utf-8"))
    os.makedirs(OUT, exist_ok=True)
    rules = cfg["rules"]
    externals = [r for r in rules if r["owner"] == "external"]
    paths = [r["path"] for r in externals]
    verified = set(cfg["verifiedUrls"])
    missing = [p for p in paths if not path_exists(p)]
    not_verified = [p for p in paths if p not in verified]
    probes = [route(rules, t) for t in cfg["probes"]]
    routed = [p for p in probes if p["owner"]]

    digest = hashlib.sha256("\n".join(
        f"{r['id']}|{r['owner']}|{r['path']}" for r in rules).encode("utf-8")).hexdigest()[:16]

    summary = {
        "project": cfg["project"], "ruleSet": cfg["ruleSet"],
        "ruleCount": len(rules), "externalRuleCount": len(externals),
        "distinctExternalPaths": len(set(paths)), "thisBookRuleCount": len(rules) - len(externals),
        "pathsPairwiseDistinct": len(paths) == len(set(paths)),
        "allPathsInVerifiedList": not not_verified, "allPathsInPublic": not missing,
        "missingPaths": missing, "probes": probes, "routedCount": len(routed),
        "digest": digest,
    }
    with open(os.path.join(OUT, "ta06_scope.json"), "w", encoding="utf-8") as f:
        json.dump(summary, f, ensure_ascii=False, indent=2, sort_keys=True)
        f.write("\n")

    print(f"rules={len(rules)} external={len(externals)} thisBook={len(rules) - len(externals)}")
    print(f"distinctExternalPaths={len(set(paths))} pairwiseDistinct={summary['pathsPairwiseDistinct']} "
          f"allInVerified={summary['allPathsInVerifiedList']} allInPublic={summary['allPathsInPublic']}")
    for p in probes:
        print(f"  {p['topic']:<16} -> {str(p['owner']):<10} {p['path'] or '-'} ({p['ruleId'] or '-'})")
    print(f"digest={digest}")
    print("wrote out/ta06_scope.json")
    ok = summary["pathsPairwiseDistinct"] and summary["allPathsInVerifiedList"] and summary["allPathsInPublic"]
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())

实跑 /opt/homebrew/bin/python3 sim/ta06_scope.py:

rules=6 external=3 thisBook=3
distinctExternalPaths=3 pairwiseDistinct=True allInVerified=True allInPublic=True
  着色器编译            -> external   /graphics-shader-compilation-pipeline/ (shader-compile)
  shader 变体爆炸      -> external   /graphics-mesh-shaders-nextgen/ (shader-variant)
  渲染管线原理           -> external   /graphics-render-graph/ (render-pipeline)
  材质参数表            -> this-book  - (material-param-table)
  参数对齐与打包          -> this-book  - (param-packing)
  材质槽预算            -> this-book  - (material-slot-budget)
  完全没听过的主题         -> None       - (-)
digest=7285b015fed5ee53
wrote out/ta06_scope.json

3 条 external、3 条 this-book;三条外链路径两两不同、全在已核验清单内、且都在 public/ 产物里真实存在(allInPublic=True)。探针里「着色器编译」判给 external、「参数对齐与打包」判给 this-book、「完全没听过的主题」返回 None——越界与漏网都被这张表挡住。

6.3.5 P 层内核

同一份路由表,TypeScript 侧独立实现一遍:

// file: core/src/ta06/scope.ts
// tier: P
// verify: node --test 'core/test/ta06_*.test.ts'
// 6.3 第 6 章划界路由表的 TS 内核。与 sim/ta06_scope.py 同一套数据(config/ta06_scope.json):
// 首个命中的规则胜出,external 规则必须排在 this-book 之前。external 路径取自已核验清单。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。

export type Owner = "this-book" | "external";
export type ScopeRule = { id: string; owner: Owner; path: string | null; match: string[] };
export type RouteResult = { topic: string; owner: Owner | null; path: string | null; ruleId: string | null };

export function route(rules: ScopeRule[], topic: string): RouteResult {
  for (const r of rules) {
    if (r.match.some((k) => topic.includes(k))) {
      return { topic, owner: r.owner, path: r.path, ruleId: r.id };
    }
  }
  return { topic, owner: null, path: null, ruleId: null };
}

export function externalRules(rules: ScopeRule[]): ScopeRule[] {
  return rules.filter((r) => r.owner === "external");
}

export function externalPaths(rules: ScopeRule[]): string[] {
  return externalRules(rules).map((r) => r.path as string);
}

6.3.6 P 层测试:不变量与负例

P 层测试断言 §9.1 的三条不变量,并用「朴素路由器」做负例:

// file: core/test/ta06_scope.test.ts
// tier: P
// verify: node --test 'core/test/ta06_*.test.ts'
// 划界路由表断言:external 路径两两不同、条数等于去重条数、全部在已核验清单内;探针路由与
// Python 产物逐位一致。负例是核心 —— 朴素路由器把「着色器编译」判成 this-book,必须被拦住。
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 { route, externalRules, externalPaths } from "../src/ta06/scope.ts";
import type { ScopeRule, Owner } from "../src/ta06/scope.ts";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const cfg = JSON.parse(readFileSync(resolve(ROOT, "config/ta06_scope.json"), "utf8"));
const py = JSON.parse(readFileSync(resolve(ROOT, "out/ta06_scope.json"), "utf8"));
const rules = cfg.rules as ScopeRule[];

test("不变量:external 路径两两不同、条数等于去重条数、全部在已核验清单内", () => {
  const paths = externalPaths(rules);
  assert.equal(paths.length, new Set(paths).size, "external 路径有重复");
  assert.equal(externalRules(rules).length, paths.length, "external 规则数与路径数不符");
  const verified = new Set(cfg.verifiedUrls as string[]);
  for (const p of paths) assert.ok(verified.has(p), `${p} 不在已核验清单`);
  assert.equal(py.pathsPairwiseDistinct, true);
  assert.equal(py.allPathsInVerifiedList, true);
  assert.equal(py.allPathsInPublic, true);
});

test("跨语言:TS 探针路由与 Python 产物逐位一致", () => {
  const mine = (cfg.probes as string[]).map((t) => route(rules, t));
  assert.deepEqual(mine, py.probes);
  assert.equal(py.routedCount, 6);
  assert.equal(py.digest, "7285b015fed5ee53");
});

test("正例:本章地盘判给 this-book,原理判给 external", () => {
  assert.equal(route(rules, "材质参数表")?.owner, "this-book");
  assert.equal(route(rules, "参数对齐与打包")?.owner, "this-book");
  assert.equal(route(rules, "材质槽预算")?.owner, "this-book");
  assert.equal(route(rules, "着色器编译")?.owner, "external");
  assert.equal(route(rules, "shader 变体爆炸")?.owner, "external");
  assert.equal(route(rules, "渲染管线原理")?.owner, "external");
});

test("负例:朴素路由器把「着色器编译」判成 this-book,真路由表必须不这么判", () => {
  // 朴素路由器:见到「编译」就收进本卷 —— 正是划界要防的越界
  const naive = (t: string): Owner => (t.includes("编译") || t.includes("管线") ? "this-book" : "external");
  assert.equal(naive("着色器编译"), "this-book"); // 朴素路由器确实会错判
  const r = route(rules, "着色器编译");
  assert.ok(r);
  assert.notEqual(r.owner, "this-book");
  assert.equal(r.owner, "external");
  assert.equal(r.path, "/graphics-shader-compilation-pipeline/");
});

test("负例:「参数对齐」含「对齐」但不得被任何 external 规则抢走", () => {
  const r = route(rules, "参数对齐");
  assert.equal(r.ruleId, "param-packing");
  assert.equal(r.owner, "this-book");
});

test("未覆盖主题返回 null(需要新增规则,而不是静默归到本章)", () => {
  assert.equal(route(rules, "完全没听过的主题").owner, null);
});

实跑 node --test 'core/test/ta06_*.test.ts'(ta06_* 三节共 13 条,此处只列本节相关的 6 条与汇总):

✔ 不变量:external 路径两两不同、条数等于去重条数、全部在已核验清单内
✔ 跨语言:TS 探针路由与 Python 产物逐位一致
✔ 正例:本章地盘判给 this-book,原理判给 external
✔ 负例:朴素路由器把「着色器编译」判成 this-book,真路由表必须不这么判
✔ 负例:「参数对齐」含「对齐」但不得被任何 external 规则抢走
✔ 未覆盖主题返回 null(需要新增规则,而不是静默归到本章)
ℹ tests 13
ℹ pass 13
ℹ fail 0

digest=7285b015fed5ee53,两侧逐位一致。这张表不是文档,是门禁:新增一条「属于本章」的主题时,先改 config/ta06_scope.json 再跑两侧;判不出来的主题返回 null,构建期就会提醒你。边界靠断言守住,不靠自觉。

阅读导航:上一节:6.2 参数对齐与打包 · 下一节:7.1 LOD 分级与误差指标 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「gameart」更多文章

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