站内 《移动端与 PC 的美术性能预算》 讲的是「一台机器能画多少面」,第一卷 7.1 面数与骨骼分级预算 讲的是「一件资产允许多少面」。两者都回答「上限是多少」,本节回答另一个问题:同一个品类里,这些上限要乘多少件,才算得出一支团队一个迭代的真实美术预算。产能口径直接复用卷二 14.1 产能度量:人天与吞吐 的有效系数与人天×100 记账法,不另造一套数字。
14.1 品类与美术预算的关系
14.1.1 品类四分
行业里「品类」这个词在不同场合指不同东西。本节按美术产能的口径把它分成四格,与站内既有品类讨论的用法保持一致:
| 品类 | 典型规模 | 美术特征 | 产能含义 |
|---|---|---|---|
| 休闲 | 小团队、短迭代 | 件数少、单件便宜、细节低 | 单位产能出的件数最多 |
| 中度 | 中等团队、标准迭代 | 件数与单件成本都居中 | 基准档 |
| 重度 | 大团队、高规格 | 件数多、单件贵、档位高 | 单位产能出的几何量最少 |
| 独立 | 极小团队、长周期 | 件数少但单件慢 | 用「少人 × 长周期」换产能 |
四格的分界不是「好玩不好玩」,而是三个可量化的东西:迭代规模(人数 × 工作日)、资产需求(件数 × 单件人天×100)、档位分布(每件资产落在哪个面数档)。把这三样写成数据,品类与预算的关系就从「重度项目美术贵」这句废话,变成一张能逐格对账的表。
14.1.2 D 层:四品类的迭代规模与资产需求
下面这份配置是 D 层的数据模型。它给四个品类各一份「一个迭代」的输入,tiers 里的档位上限对共享真源 config/visual_spec.json 的 budget 段做逐位对齐(道具档 = trisLow、主要角色档 = trisMid)。
{
"project": "stardust-inn",
"chapter": 14,
"section": "14.1",
"specRef": "config/visual_spec.json",
"note": "品类与美术预算的关系。四分品类(休闲/中度/重度/独立)各给一个「一个迭代」的团队规模与资产需求。产能口径与卷二 14.1 同源:有效系数 = 出勤率‰ × 专注率‰ // 1000 = 850 × 700 // 1000 = 595‰,人天一律 ×100 整数。档位上限 tiers[].maxTris 里 prop 直接对齐只读真源 config/visual_spec.json 的 budget.trisLow(1500),npc-major 对齐 budget.trisMid(6000),另两档是本节新增的章级参数。所有团队规模、件数与单件成本都是本套构造的示例参数,不代表任何真实团队或公司。全整数,跨语言逐位一致。",
"caliber": {
"availabilityPermille": 850,
"focusPermille": 700,
"sourceRef": "art-ta-14-1-capacity-person-day-throughput"
},
"tiers": [
{ "id": "prop", "cn": "道具与家具", "maxTris": 1500, "specKey": "trisLow" },
{ "id": "npc-minor", "cn": "次要角色", "maxTris": 3000, "specKey": null },
{ "id": "npc-major", "cn": "主要角色", "maxTris": 6000, "specKey": "trisMid" },
{ "id": "hero", "cn": "主角", "maxTris": 12000, "specKey": null }
],
"genres": [
{
"id": "casual",
"cn": "休闲",
"people": 4,
"workdays": 10,
"demand": [
{ "id": "furn_icon", "cn": "家具图标", "units": 8, "costPerUnit100": 50, "tier": "prop" },
{ "id": "furn_prefab", "cn": "家具 prefab", "units": 2, "costPerUnit100": 180, "tier": "prop" },
{ "id": "portrait", "cn": "角色头像", "units": 1, "costPerUnit100": 300, "tier": "npc-major" },
{ "id": "ui_icon", "cn": "UI 图标", "units": 20, "costPerUnit100": 30, "tier": "prop" },
{ "id": "vfx_burst", "cn": "特效", "units": 1, "costPerUnit100": 200, "tier": "prop" }
]
},
{
"id": "mid-core",
"cn": "中度",
"people": 6,
"workdays": 10,
"demand": [
{ "id": "furn_icon", "cn": "家具图标", "units": 12, "costPerUnit100": 60, "tier": "prop" },
{ "id": "furn_prefab", "cn": "家具 prefab", "units": 4, "costPerUnit100": 250, "tier": "prop" },
{ "id": "portrait", "cn": "角色头像", "units": 2, "costPerUnit100": 400, "tier": "npc-major" },
{ "id": "ui_icon", "cn": "UI 图标", "units": 24, "costPerUnit100": 40, "tier": "prop" },
{ "id": "vfx_burst", "cn": "特效", "units": 1, "costPerUnit100": 300, "tier": "prop" }
]
},
{
"id": "hard-core",
"cn": "重度",
"people": 12,
"workdays": 10,
"demand": [
{ "id": "furn_icon", "cn": "家具图标", "units": 18, "costPerUnit100": 80, "tier": "prop" },
{ "id": "furn_prefab", "cn": "家具 prefab", "units": 8, "costPerUnit100": 320, "tier": "prop" },
{ "id": "portrait", "cn": "角色头像", "units": 3, "costPerUnit100": 500, "tier": "hero" },
{ "id": "ui_icon", "cn": "UI 图标", "units": 30, "costPerUnit100": 50, "tier": "prop" },
{ "id": "vfx_burst", "cn": "特效", "units": 2, "costPerUnit100": 400, "tier": "prop" }
]
},
{
"id": "indie",
"cn": "独立",
"people": 2,
"workdays": 30,
"demand": [
{ "id": "furn_icon", "cn": "家具图标", "units": 10, "costPerUnit100": 70, "tier": "prop" },
{ "id": "furn_prefab", "cn": "家具 prefab", "units": 3, "costPerUnit100": 260, "tier": "prop" },
{ "id": "portrait", "cn": "角色头像", "units": 2, "costPerUnit100": 420, "tier": "npc-major" },
{ "id": "ui_icon", "cn": "UI 图标", "units": 18, "costPerUnit100": 45, "tier": "prop" },
{ "id": "vfx_burst", "cn": "特效", "units": 1, "costPerUnit100": 320, "tier": "prop" }
]
}
]
}
配置里有两个设计点值得说明。第一,四个品类共用同一份档位表:档位是「一件资产允许多少面」,由风格与平台决定,不由品类决定;品类决定的是每档各要多少件。重度品类把角色头像放到 hero 档(12000 面),休闲品类只放到 npc-major 档(6000 面),差的就是这一格。第二,独立品类用 2 人 × 30 天换到与中度 6 人 × 10 天相同的有效产能——人数少不等于产能低,把周期拉长可以补回来,但补不回来的是「同时并行的角色数」(见卷二 14.1 的「产能不能跨角色平移」)。
14.1.3 S 层:五笔整数账
下面这段是 S 层脚本,读上面的配置与只读真源 visual_spec.json,对四个品类各算五笔账:有效产能、需求人天、利用率、吞吐与几何预算。全整数、地板除。
#!/usr/bin/env python3
# file: sim/st14_budget.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/st14_budget.py
"""14.1 品类与美术预算的关系(S 层,卷三第 14 章)。
读 config/st14_genre_budget.json(四个品类的迭代规模 + 资产需求 + 档位上限)与只读真源
config/visual_spec.json 的 budget 段(做档位对齐断言),算五笔整数账:
1. **有效产能**:有效系数 = 出勤率‰ × 专注率‰ // 1000;日历人天×100 = 人数 × 工作日 × 100;
有效人天×100 = 日历人天×100 × 有效系数‰ // 1000(与卷二 14.1 同一套口径,不另造);
2. **需求人天×100** = Σ 件数 × 单件人天×100;
3. **利用率‰** = 需求人天×100 × 1000 // 有效人天×100(>1000 即超产能);
4. **吞吐×100** = 件数 × 10000 // 有效人天×100(每有效人天的资产数);
5. **几何预算** = Σ 件数 × 该档位 tris 上限(档位上限里 prop / npc-major 与共享真源逐位对齐)。
再算「每有效人天的 tris 产出×100」把品类与几何预算挂上钩。全整数、地板除,跨语言逐位一致。
所有团队规模与成本都是本套构造的示例参数,真实产能需真实生产环境,命令行不可复现(X)。
产出 out/st14_budget.json + out/st14_budget.csv。
"""
import csv
import hashlib
import json
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CFG = os.path.join(ROOT, "config/st14_genre_budget.json")
SPEC = os.path.join(ROOT, "config/visual_spec.json")
OUT = os.path.join(ROOT, "out")
def align_tiers(spec_budget, tiers):
"""档位上限与共享真源 visual_spec.json 的 budget 段逐位对齐(只对声明了 specKey 的档)。"""
rows = []
for t in tiers:
key = t.get("specKey")
if key:
rows.append({"tier": t["id"], "specKey": key,
"lhs": t["maxTris"], "rhs": spec_budget[key]})
return rows
def genre_account(g, factor, tier_cap):
"""单个品类的整数账。"""
calendar100 = g["people"] * g["workdays"] * 100
effective100 = calendar100 * factor // 1000
demand100 = sum(d["units"] * d["costPerUnit100"] for d in g["demand"])
units = sum(d["units"] for d in g["demand"])
tris = sum(d["units"] * tier_cap[d["tier"]] for d in g["demand"])
by_tier = {}
for d in g["demand"]:
by_tier[d["tier"]] = by_tier.get(d["tier"], 0) + d["units"]
return {
"id": g["id"], "cn": g["cn"], "people": g["people"], "workdays": g["workdays"],
"calendar100": calendar100, "effective100": effective100,
"demand100": demand100, "units": units, "tris": tris,
"utilPermille": demand100 * 1000 // effective100 if effective100 else 0,
"naiveUtilPermille": demand100 * 1000 // calendar100 if calendar100 else 0,
"throughput100": units * 10000 // effective100 if effective100 else 0,
"avgCost100": demand100 // units if units else 0,
"trisPerDayX100": tris * 100 // effective100 if effective100 else 0,
"byTier": by_tier,
}
def main() -> int:
cfg = json.load(open(CFG, encoding="utf-8"))
spec = json.load(open(SPEC, encoding="utf-8"))
factor = cfg["caliber"]["availabilityPermille"] * cfg["caliber"]["focusPermille"] // 1000
tier_cap = {t["id"]: t["maxTris"] for t in cfg["tiers"]}
align = align_tiers(spec["budget"], cfg["tiers"])
misaligned = [r["tier"] for r in align if r["lhs"] != r["rhs"]]
rows = [genre_account(g, factor, tier_cap) for g in cfg["genres"]]
lines = [f'{r["id"]}:{r["effective100"]}:{r["demand100"]}:{r["utilPermille"]}:{r["tris"]}'
for r in rows]
digest = hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()[:16]
report = {
"project": cfg["project"], "section": cfg["section"],
"availabilityPermille": cfg["caliber"]["availabilityPermille"],
"focusPermille": cfg["caliber"]["focusPermille"],
"effectiveFactorPermille": factor, "sourceRef": cfg["caliber"]["sourceRef"],
"tierCap": tier_cap, "tierAlign": align, "misaligned": misaligned,
"genreCount": len(rows), "genres": rows,
"digest": digest,
}
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "st14_budget.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
with open(os.path.join(OUT, "st14_budget.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["genre", "cn", "people", "workdays", "effective100", "demand100",
"units", "utilPermille", "throughput100", "avgCost100", "tris"])
for r in rows:
w.writerow([r["id"], r["cn"], r["people"], r["workdays"], r["effective100"],
r["demand100"], r["units"], r["utilPermille"], r["throughput100"],
r["avgCost100"], r["tris"]])
print(f'factor={factor}permille tiers={tier_cap} misaligned={misaligned}')
for r in rows:
print(f' {r["id"]:<10} ppl={r["people"]:>2} days={r["workdays"]:>2} '
f'eff100={r["effective100"]:>5} dem100={r["demand100"]:>5} '
f'util={r["utilPermille"]:>4}permille units={r["units"]:>2} '
f'thr100={r["throughput100"]:>4} avgCost100={r["avgCost100"]:>3} '
f'tris={r["tris"]:>6} trisPerDayX100={r["trisPerDayX100"]:>5}')
print(f'digest={digest}')
print("wrote out/st14_budget.json out/st14_budget.csv")
return 1 if misaligned else 0
if __name__ == "__main__":
sys.exit(main())
脚本开头就把档位上限与共享真源对齐(misaligned=[] 才继续),这是本套「单一真源」的常规护栏:同一份 visual_spec.json 被四卷、多章消费,任何一处自己写死一个 1500 都会让对账失效。
14.1.4 实跑输出
本机实跑 /opt/homebrew/bin/python3 sim/st14_budget.py(同机相对量级,绝对数值不可移植):
factor=595permille tiers={'prop': 1500, 'npc-minor': 3000, 'npc-major': 6000, 'hero': 12000} misaligned=[]
casual ppl= 4 days=10 eff100= 2380 dem100= 1860 util= 781permille units=32 thr100= 134 avgCost100= 58 tris= 52500 trisPerDayX100= 2205
mid-core ppl= 6 days=10 eff100= 3570 dem100= 3780 util=1058permille units=43 thr100= 120 avgCost100= 87 tris= 73500 trisPerDayX100= 2058
hard-core ppl=12 days=10 eff100= 7140 dem100= 7800 util=1092permille units=61 thr100= 85 avgCost100=127 tris=123000 trisPerDayX100= 1722
indie ppl= 2 days=30 eff100= 3570 dem100= 3450 util= 966permille units=34 thr100= 95 avgCost100=101 tris= 60000 trisPerDayX100= 1680
digest=823e80b588fa77d6
wrote out/st14_budget.json out/st14_budget.csv
14.1.5 读表:品类改的是哪几个数
把四行摊成一张表(人天单位是 ×100):
| 品类 | 人数 × 天 | 有效人天 | 需求人天 | 利用率 | 吞吐 | 平均单件 | 几何预算 | tris/人天 |
|---|---|---|---|---|---|---|---|---|
休闲 casual | 4 × 10 | 2380 | 1860 | 781‰ | 134 | 58 | 52500 | 2205 |
中度 mid-core | 6 × 10 | 3570 | 3780 | 1058‰ | 120 | 87 | 73500 | 2058 |
重度 hard-core | 12 × 10 | 7140 | 7800 | 1092‰ | 85 | 127 | 123000 | 1722 |
独立 indie | 2 × 30 | 3570 | 3450 | 966‰ | 95 | 101 | 60000 | 1680 |
四条结论:
- 品类首先改的是「平均单件成本」:58 → 87 → 127,从休闲到重度翻了约 2.2 倍。原因是同一类资产在更重的品类里被放到更高档位、要更多细节与变体。品类预算的差异,大半藏在单件成本里,而不是件数里。
- 吞吐反向下降:134 → 120 → 85,每有效人天能出的件数从 1.34 掉到 0.85。重度品类不是「多做几件」,而是「每件都更贵」。
tris/人天也在下降:2205 → 2058 → 1722。这有点反直觉——重度资产的面数更高,为什么单位产能出的面数反而更少?因为面数涨得比人天慢:角色头像从 6000 面升到 12000 面(×2),人天却从 400 涨到 500(×1.25)再叠加细节工,净效应是每单位产能承载的几何量下降。这说明「品类越重 → 几何预算越大」这句话只在总量层面成立,在单位产能层面是反的。- 独立档用长周期换产能:2 人 × 30 天与 6 人 × 10 天都拿到
3570有效人天,利用率966‰甚至比中度更松。人数少不等于排不下,但独立档的件数少、单件贵,几何预算只有60000——省下的是总量,不是单件。
14.1.6 边界
- 所有数字都是示例参数。团队规模、件数、单件成本都是本套构造的,用来演示算法而非给出可移植的产能基准。真实产能需真实环境(真实人员与项目数据),命令行不可复现(X)。
- 档位表是风格与平台的函数,不是品类的函数。本节只让「每档要多少件」随品类变;如果项目换平台(移动端 → 主机),档位上限本身要改,那是卷一第 7 章与卷二第 7 章的活。
- 不做「提升 N 倍」的断言。四行之间是同机、同口径的相对账,换一组示例参数整张表都会变,算法与对账口径不变。
- 利用率超过 1000‰ 不是错误,是信号。中度
1058‰与重度1092‰都超产能,说明这份需求按当前团队规模排不下;能动的杠杆(加人 / 砍范围 / 提系数)在卷二 14.1 已算过,本节只负责把品类这一层接上去。
14.1.7 自检表
| 检查项 | 合格标准 |
|---|---|
| 口径同源 | 有效系数 595‰ 与人天×100 直接复用卷二 14.1,不另造 |
| 档位对齐 | prop / npc-major 上限与 visual_spec.json 的 trisLow / trisMid 逐位一致 |
| 全整数 | 人天、件数、面数、千分比全整数,除法一律地板除 |
| 边界诚实 | 团队规模与成本标 X(示例参数),不写成「实测」 |
| 可对账 | 落 JSON + CSV,同一份数据既是账本又是下游 14.2 / 14.3 的输入 |
品类这一层接上之后,预算账就完整了:档位决定单件上限,品类决定件数与单件成本,两者相乘才是美术预算。下一节换一个角度,看品类怎样决定「同一份调色板算不算可读」。
阅读导航:上一节:13.3 主题切换与变体管理 · 下一节:14.2 品类与视觉可读性 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。