站内 渲染原理专题
(45 篇)是渲染原理的家——光栅化、着色、后处理、色彩管理 HDR 都在那里讲透;《渲染引擎》卷首的分工说明
把「引擎实现」与「渲染原理」的边界划得很清楚。本套不重推任何一条渲染原理,也不重推引擎实现:本书只回答一个更靠前、也更常被跳过的问题——一件美术资产,凭什么算「做完了」。原理归 graphics/,实现归引擎卷,规范归这一卷。
四段链路,不是四个部门
新手最容易犯的错,是把「概念 → 制作 → 接入 → 验收」当成四个部门的先后顺序:概念组做完交给制作组,制作组做完交给程序。真做起来你会发现,这四段是一条链路上的四道门,每道门只认「出口交付物齐不齐」,跟谁坐在哪个工位无关。一个人可以连过两道门,一个部门也可能同时卡在两道门上。
| 阶段 | 一句话定义 | 出口交付物(缺一即卡) | 常见误区 |
|---|---|---|---|
| 概念 concept | 把形容词变成可对齐的画面 | 参考板 moodboard、风格帧 styleframe | 用「差不多这样」代替参考图 |
| 制作 production | 产出符合规格的资产本体 | 模型 model、贴图 texture | 边做边改风格,返工吃掉预算 |
| 接入 integration | 让资产在引擎里被正确引用 | 导出件 export、清单项 manifest | 把「导入报错」当成程序的问题 |
| 验收 acceptance | 判定资产是否可发布 | 截图 screenshot、签字 signoff | 进引擎后才发现不合规 |
四段里只有「制作」是真在画画,另外三段都是「判定」。这正是本章要立的第一条规矩:判定必须有交付物作凭据,不能靠口头。
每段的出口交付物:一份可校验的链路 schema
下面这段是 D 层链路 schema,把四段链路的容量与出口交付物记成一份数据。它是后面所有门禁判定的输入,也是本套「一份规范、三处消费」里被校验器消费的那一份。数值是建模输入,不是任何团队的生产实测。
{
"version": 1,
"project": "stardust-inn",
"note": "美术生产链路的阶段容量与需求。capacityPerWeek=每阶段每周可处理的资产件数(整数,建模假设);demand=一次性投放的需求件数;reworkPermille=验收阶段返工回制作阶段的千分比;weeks=仿真周数。全部为「可复现的链路模型」口径;真实画师产能与返工率需真实生产环境,命令行不可复现(X)。全整数,无浮点。",
"demand": 96,
"weeks": 20,
"reworkPermille": 180,
"reworkTarget": "production",
"stages": [
{ "id": "concept", "name": "概念", "capacityPerWeek": 12, "exit": ["moodboard", "styleframe"] },
{ "id": "production", "name": "制作", "capacityPerWeek": 9, "exit": ["model", "texture"] },
{ "id": "integration", "name": "接入", "capacityPerWeek": 15, "exit": ["export", "manifest"] },
{ "id": "acceptance", "name": "验收", "capacityPerWeek": 18, "exit": ["screenshot", "signoff"] }
]
}
读这份 schema 要抓住两点。第一,exit 数组就是门禁清单:concept 段没有 styleframe,就不许进 production——不是「建议补」,是「物理上过不去」。第二,reworkPermille 是返工回流:验收不合格的件按千分比退回制作段,这条边是链路里最贵的,因为它同时吃掉制作产能和验收产能。
S 层:链路吞吐仿真
同一份 config/sp01_pipeline.json,P 层读它做门禁判定,S 层读它做吞吐仿真——这正是「一份规范、多处消费」。全整数地板除,无浮点。本机跑 /opt/homebrew/bin/python3 sim/sp01_pipeline_flow.py,真实输出:瓶颈在 production(9 件/周),峰值在制品 peakWip=96,返工 totalRework=12,第 17 周才全部验收完毕(completionWeek=17)。96 件需求被 9 件/周的制作产能死死卡住——这就是为什么「加人」往往无效:瓶颈不在人最多的那一段。真实画师产能与返工率需真实生产环境,命令行不可复现(X)。
#!/usr/bin/env python3
# file: sim/sp01_pipeline_flow.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/sp01_pipeline_flow.py
"""美术生产链路吞吐仿真:读 config/sp01_pipeline.json,用整数地板除算四段链路
(概念→制作→接入→验收)的周吞吐、在制品 WIP、返工量与完成周数。需求在周 0 一次性
投放第一阶段,每阶段每周最多 capacityPerWeek 件,同一周从后往前结算(禁止同周穿透),
验收按 reworkPermille‰ 返工回 reworkTarget。全整数、无浮点。真实画师产能与返工率需
真实生产环境,命令行不可复现(X)。产出 out/ 下 json + csv + svg。"""
import csv
import json
import os
def simulate(stages, demand, weeks, rp, target):
order = [s["id"] for s in stages]
cap = {s["id"]: s["capacityPerWeek"] for s in stages}
q = dict.fromkeys(order, 0)
q[order[0]] = demand
acc = rew = 0
weekly = []
for w in range(weeks):
for sid in reversed(order):
n = min(cap[sid], q[sid])
q[sid] -= n
i = order.index(sid)
if i + 1 < len(order):
q[order[i + 1]] += n
else:
b = n * rp // 1000
q[target] += b
rew += b
acc += n - b
weekly.append({"week": w + 1, "wip": sum(q.values()), "accepted": acc, "rework": rew})
return weekly
def main():
cfg = json.load(open("config/sp01_pipeline.json", encoding="utf-8"))
st, dm, wk = cfg["stages"], cfg["demand"], cfg["weeks"]
weekly = simulate(st, dm, wk, cfg["reworkPermille"], cfg["reworkTarget"])
bn = min(st, key=lambda s: s["capacityPerWeek"])
last = weekly[-1]
comp = next((r["week"] for r in weekly if r["accepted"] >= dm), None)
os.makedirs("out", exist_ok=True)
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="256">' % (8 + 30 * len(weekly))
svg += "".join(f'<rect x="{8 + i * 30}" y="{236 - 2 * r["wip"]}" width="24" height="{2 * r["wip"]}"'
f' fill="#93c5fd"/>' for i, r in enumerate(weekly))
open("out/sp01_pipeline_flow.svg", "w", encoding="utf-8").write(svg + "</svg>")
rep = {"version": cfg["version"], "demand": dm, "weeks": wk, "bottleneck": bn["id"],
"bottleneckCapacity": bn["capacityPerWeek"], "reworkPermille": cfg["reworkPermille"],
"peakWip": max(r["wip"] for r in weekly), "totalRework": last["rework"],
"totalAccepted": last["accepted"], "completionWeek": comp, "weekly": weekly}
json.dump(rep, open("out/sp01_pipeline_flow.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
with open("out/sp01_pipeline_flow.csv", "w", encoding="utf-8", newline="") as f:
wr = csv.writer(f)
wr.writerow(["week", "wip", "accepted", "rework"])
for r in weekly:
wr.writerow([r["week"], r["wip"], r["accepted"], r["rework"]])
print(json.dumps({k: rep[k] for k in ("bottleneck", "bottleneckCapacity", "peakWip", "totalRework",
"totalAccepted", "completionWeek")}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
P 层:阶段门禁校验器
下面这段是 P 层 TypeScript 内核 core/src/sp01/pipeline.ts。它把上面的 schema 变成可断言的函数:canAdvance 判断一件资产能否推进到下一阶段,pipelineStatus 做整批资产的卡点归因。
// file: core/src/sp01/pipeline.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// 美术生产链路的阶段门禁校验器:概念 → 制作 → 接入 → 验收。
// 每个阶段有「出口交付物」清单;资产只有交付物齐全才允许推进到下一阶段。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。
export type StageId = "concept" | "production" | "integration" | "acceptance";
export type DeliverableId =
| "moodboard" | "styleframe"
| "model" | "texture"
| "export" | "manifest"
| "screenshot" | "signoff";
export const STAGE_ORDER: StageId[] = ["concept", "production", "integration", "acceptance"];
/** 每个阶段的出口交付物:缺任何一项即「卡在门禁前」。 */
export const STAGE_GATES: Record<StageId, DeliverableId[]> = {
concept: ["moodboard", "styleframe"],
production: ["model", "texture"],
integration: ["export", "manifest"],
acceptance: ["screenshot", "signoff"],
};
export type AssetRecord = {
id: string;
stage: StageId;
deliverables: DeliverableId[];
};
export function stageIndex(s: StageId): number {
return STAGE_ORDER.indexOf(s);
}
export function gateFor(s: StageId): DeliverableId[] {
return STAGE_GATES[s];
}
/** 资产能否从当前阶段推进到下一阶段:出口交付物必须齐全,且当前不是终点。 */
export function canAdvance(rec: AssetRecord): { ok: boolean; next: StageId | null; missing: DeliverableId[] } {
const i = stageIndex(rec.stage);
const missing = STAGE_GATES[rec.stage].filter((d) => !rec.deliverables.includes(d));
const next = i + 1 < STAGE_ORDER.length ? STAGE_ORDER[i + 1] : null;
return { ok: missing.length === 0 && next !== null, next, missing };
}
/** 链路完整度:各阶段资产分布 + 卡点(缺交付物最多的阶段,平局取更靠前的阶段)。 */
export function pipelineStatus(recs: AssetRecord[]): {
byStage: Record<StageId, number>;
blocked: number;
bottleneck: StageId;
} {
const byStage: Record<StageId, number> = { concept: 0, production: 0, integration: 0, acceptance: 0 };
const miss: Record<StageId, number> = { concept: 0, production: 0, integration: 0, acceptance: 0 };
let blocked = 0;
for (const r of recs) {
byStage[r.stage] += 1;
const m = canAdvance(r);
if (!m.ok) {
blocked += 1;
miss[r.stage] += 1;
}
}
let bottleneck: StageId = STAGE_ORDER[0];
for (const s of STAGE_ORDER) {
if (miss[s] > miss[bottleneck]) bottleneck = s;
}
return { byStage, blocked, bottleneck };
}
这里有一个刻意的设计:canAdvance 在终点阶段(acceptance)永远返回 ok: false,因为 next 是 null。别把它当 bug——「没有下一阶段可推」本身就是链路走完的信号,把终点当成可推进会让卡点归因把已完成的资产也数进去。
负例:缺交付物必须真的被拦下
一个门禁最危险的失败模式不是「报错」,而是该拦的没拦。所以这段 P 层测试的重点全在负例上。
// file: core/test/sp01_pipeline.test.ts
// tier: P
// verify: node --test 'core/test/sp01_*.test.ts'
// 阶段门禁的断言:D 层链路配置与 TS 门禁一致、四段顺序固定、交付物齐全才推进、终点无下一阶段。
// 负例是重点 —— 缺交付物必须真的被拦下,不能静默通过。
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 {
STAGE_ORDER,
STAGE_GATES,
stageIndex,
gateFor,
canAdvance,
pipelineStatus,
} from "../src/sp01/pipeline.ts";
import type { AssetRecord } from "../src/sp01/pipeline.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(HERE, "../..");
const cfg = JSON.parse(readFileSync(resolve(ROOT, "config/sp01_pipeline.json"), "utf8"));
test("D 层链路配置与 TS 门禁一致(防漂移)", () => {
assert.deepEqual(cfg.stages.map((s: { id: string }) => s.id), STAGE_ORDER);
for (const s of cfg.stages) assert.deepEqual(STAGE_GATES[s.id], s.exit, `${s.id} 门禁不一致`);
});
test("阶段顺序固定为四段", () => {
assert.deepEqual(STAGE_ORDER, ["concept", "production", "integration", "acceptance"]);
assert.equal(stageIndex("concept"), 0);
assert.equal(stageIndex("acceptance"), 3);
});
test("每段门禁非空且交付物不重复", () => {
for (const s of STAGE_ORDER) {
const g = gateFor(s);
assert.ok(g.length >= 2, `${s} 门禁过少`);
assert.equal(new Set(g).size, g.length, `${s} 交付物重复`);
}
assert.deepEqual(STAGE_GATES.concept, ["moodboard", "styleframe"]);
});
test("交付物齐全才允许推进,且给出下一阶段", () => {
const ok = canAdvance({ id: "a", stage: "concept", deliverables: ["moodboard", "styleframe"] });
assert.equal(ok.ok, true);
assert.equal(ok.next, "production");
assert.deepEqual(ok.missing, []);
});
test("负例:缺 styleframe 时概念阶段不得推进", () => {
const bad = canAdvance({ id: "b", stage: "concept", deliverables: ["moodboard"] });
assert.equal(bad.ok, false);
assert.notEqual(bad.ok, true);
assert.deepEqual(bad.missing, ["styleframe"]);
});
test("负例:验收是链路终点,无下一阶段", () => {
const end = canAdvance({
id: "c",
stage: "acceptance",
deliverables: ["screenshot", "signoff"],
});
assert.equal(end.next, null);
assert.equal(end.ok, false);
});
test("卡点归因:缺交付物最多的阶段被识别", () => {
const recs: AssetRecord[] = [
{ id: "a", stage: "production", deliverables: ["model"] },
{ id: "b", stage: "production", deliverables: [] },
{ id: "c", stage: "concept", deliverables: ["moodboard", "styleframe"] },
];
const st = pipelineStatus(recs);
assert.equal(st.byStage.production, 2);
assert.equal(st.byStage.concept, 1);
assert.equal(st.blocked, 2);
assert.equal(st.bottleneck, "production");
});
在本机跑 node --test 'core/test/sp01_pipeline.test.ts',真实输出(同机相对量级,耗时不可移植):
✔ D 层链路配置与 TS 门禁一致(防漂移) (2.520125ms)
✔ 阶段顺序固定为四段 (0.292833ms)
✔ 每段门禁非空且交付物不重复 (0.215166ms)
✔ 交付物齐全才允许推进,且给出下一阶段 (0.122167ms)
✔ 负例:缺 styleframe 时概念阶段不得推进 (0.146708ms)
✔ 负例:验收是链路终点,无下一阶段 (0.124209ms)
✔ 卡点归因:缺交付物最多的阶段被识别 (0.13575ms)
ℹ tests 7
ℹ pass 7
ℹ fail 0
两条负例各守一个方向:缺 styleframe 守住「该拦的必须拦」,验收是终点 守住「别把终点当中间站」。如果这两条里的任何一条从「拦下」变成「放行」,门禁就已经失效了,而不是测试写错了。
卡点归因与裁剪顺序
当一批资产卡在链路里,别急着「加人」。先按下面的顺序归因,再决定裁什么:
| 卡点 | 症状 | 正确的裁剪顺序 |
|---|---|---|
| 概念卡 | 需求含糊,风格反复推翻 | 先锁死 moodboard,没有参考板不进制作 |
| 制作卡 | 面数 / 贴图 / 材质槽超预算 | 先减面数,再降贴图分辨率,最后才减材质槽 |
| 接入卡 | 导出丢轴向 / 丢单位 / 引用悬空 | 接入期只做数值校验,不回炉改美术 |
| 验收卡 | 进引擎后才发现不合规 | 把校验前移到接入期,验收只做最终签字 |
裁剪顺序不是拍脑袋:面数是显存与填充率的大头,贴图次之,材质槽最少——先动影响最小、恢复最贵的那个,是通用原则。这一条在第 7 章「美术资产预算」里会落成可算的整数账。
自检表
| 检查项 | 合格标准 |
|---|---|
| 四段齐全 | 概念 / 制作 / 接入 / 验收各有明确定义与出口 |
| 门禁可执行 | 出口是清单不是口头承诺,且判定有可跑的校验器 |
| 判定可验 | 「缺件必须被拦」有测试锁死,且能算出卡点阶段与卡住件数 |
| 边界清晰 | 真实产能与返工率标 X,不写成「实测」 |
链路的第一个反直觉点是:它看起来是流程问题,实际是判定问题。谁做、做几周,都不如「出口交付物齐不齐」这一条硬。下一节我们把「谁判定」这件事落到角色上——同一件交付物,谁执行、谁会签、谁最终拍板,必须唯一。
阅读导航:上一节:目录 · 下一节:1.2 角色分工与职责矩阵 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。