☰
木材光照评分卡(Wood Lighting Scorecard):用锚点量化 3D 木材场景的 10 分制评测标准
2026/10/9 4:58:17 网站建设 项目流程

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载

木材只有在暗房间里被一束方向性主光斜扫过时,才真正"像木头":纹理、接触阴影和光柱由同一扇窗户照亮,最后统一做一次色调映射,每一部分都在渲染完成的帧上被量化测量,而不是靠"感觉"来评判。本篇文章以 Skills 仓库中3d-wood-lighting-scorecard技能附带的评分标准(rubric.md)为核心,完整解析这套"8 项 10 分制"评测体系:每一项如何由 2~4 个可测量子指标构成、每个子指标如何通过三个锚点[v@3, v@6, v@9]映射为分数,以及如何结合metrics.js、scorecard.mjs与真实 scorecard JSON 来诚实地阅读、验证和迭代渲染结果。读完你将掌握一套可复制到任意 Three.js 木材光照场景的客观评分方法,以及"分数会说谎"时如何自我校验。

评分卡机制:从原始像素到 0~10 分

rubric.md 的开头定义了整套评分体系的核心机制,这也是理解后面所有表格的前提:

  • 八个评分项(item),每项 0~10 分;
  • 每个评分项由2~4 个可测量的子指标(sub-metric)构成;
  • 每个子指标通过三个锚点[v@3, v@6, v@9]映射到分数——锚点之间线性插值,超出锚点范围继续外推,最终clamp 到 0~10;对于"越低越好"的指标,锚点可以按从高到低排列;
  • 评分项的总分是其所有子指标分数的平均值(在 metrics.js 中对应item()函数对parts.map(p => p.score)求均值);
  • 锚点数值与 metrics.js 中的硬编码一一对应(第 100~222 行的part()调用),rubric.md 明确警告:改动任何一个锚点,必须同时修改 metrics.js 中的对应数组,并重新运行 baseline 与 ablation 测试,否则分数将失去意义。

三个用于校准的"基准场景"描述:

  • "9":一帧 Kibori 车站的静帧(上过油的扁柏长凳、一扇窗户、光束中的灰尘),一眼看过去与照片无法区分;
  • "6":一个合格的 Three.js 场景——颜色正确、有光照、没有明显错误,但也毫无说服力;
  • "3":一个平涂着色(flat-shaded)的道具。

完整的分数语义(Scale):

分数含义
10可以拿来研究观摩
9受过训练的眼睛也几乎挑不出毛病
8打磨到位,只有细微瑕疵
7有明显缺陷
6能用,但有肉眼可见的粗糙边缘
5平庸
3~4多处明显错误
1~2几乎无法正常工作

rubric.md 还强调了区域(region)的约定:所有下述测量区域都以"帧自身的比例坐标"表示,并通过实时相机从世界坐标点投影得到(即 demo.html 中的KiboriScore.regions(),见 demo.html)。因此换一个场景打分,只需要重写regions()函数,而不需要改动任何指标逻辑——指标始终只吃一张帧的 RGBA 字节加一组区域矩形。

1. 材质与木纹(Materials and grain)

木纹是木材真实感的第一道门槛。rubric.md 给出了三个观察锚点:

分数看起来像什么
3单一平坦的棕褐色,或可明显看到重复的平铺照片;木纹线以随机角度横穿木板(uFlat开启时,实测得分 1.7)
6条纹方向正确,但每块木板图案相同;没有毛孔,没有年轮漂移
9年轮漂移成拱形(cathedral arcs),毛孔与纤维在 1 px 尺度仍可分辨,端面年轮可读出细胞结构,没有两块木板相同(demo 实测 8.1)

测量指标(在"素木的受光斑块"上测量,避开雕刻区域):

子指标说明锚点[v@3, v@6, v@9]
grainContrast1~8 px 带通标准差 / 亮度均值0.006 / 0.022 / 0.05
grainAniso垂直于木纹方向的梯度能量 / 沿木纹方向1.1 / 1.9 / 3.2(线条沿轴线方向)
toneVar29 px 模糊后的标准差 / 均值,捕捉年轮带的宽幅漂移0.008 / 0.035 / 0.08

在 metrics.js 中,这三个指标在grain区域内计算:grainContrast用 1 px 与 4 px 盒式模糊的差值做带通(Y[i] - b4);grainAniso把梯度旋转到木纹角度坐标系(ca/sa)后分别累计横向与纵向能量;toneVar用 14 px 盒式模糊(boxAt(...,14))估计宽幅亮度漂移。

实测参考(来自 last-scorecard.json):demo 该评分项 8.13 分,其中grainContrast = 0.0357 → 7.46、grainAniso = 106.48 → 10(远超锚点上界)、toneVar = 0.0491 → 6.94。而全部层关闭的 baseline 只有 1.67 分,可见"一个平坦颜色"与"程序化木纹 + 光照"之间的差距有多大。

2. 主光 / 补光对比(Key / fill contrast)

木材的"雕塑感"来自受光面与背光面的明确分离,同时背光面又不能死黑。

分数看起来像什么
3受光面与背光面相差不足一档,读起来像环境光(baseline 实测 4.1)
6有清晰的亮面,但暗面要么糊成一片、要么死黑
9暖主光高出冷补光 3 档以上,冷补光让背光侧保持"活着";房间在光池之外保持黑暗(demo 实测 10)

测量指标:

子指标说明锚点
keyFillStopslog2(受光面线性亮度 / 背光面线性亮度),单位为档0.7 / 1.7 / 2.8
rangeStops全帧 p99 / p1 的亮度比,档数3 / 5 / 7
shadeAlive背光面线性亮度 ×1000(必须不能被压到 0)1.5 / 4 / 10

源码中(metrics.js),keyFillStops先把 8-bit 亮度通过 sRGB 解伽马查找表LIN转成线性光,再取两区域均值比值的 log2;shadeAlive直接返回背光面线性亮度 ×1000,专门用来拦截"补光太弱导致背光面 crush 到 0"的典型错误。

实测:demo 该项满分 10(keyFillStops = 3.29档、rangeStops = 10.32档、shadeAlive = 44.06)。对应 SKILL.md 中的光照配置:暖主光DirectionalLight0xfff2df、强度 2.0,补光 0xaec7df 0.60~0.76,轮廓光 0xffdeb4 0.72,半球光 0x19——这些弱光项"只负责让背光侧保持存活,不是画面本身"。

3. 阴影与接触(Shadows and contact)

阴影是物体"坐进"场景的关键。rubric.md 指出两个最典型的问题:无阴影,以及物体底部与阴影之间出现亮缝(peter-panning)。

分数看起来像什么
3无阴影,或阴影与物体之间有亮缝(baseline 实测 5.6)
6阴影落下了,但是均匀灰色;每个物体的底部都有一点"漂浮"
9阴影起始处深、向外散开成柔和的半影(penumbra),物体在接触处有一条暗缝(demo 实测 9.6)

测量指标(4 个子指标,是该技能中维度最丰富的一项):

子指标说明锚点
shadowRatio受光长凳 / 阴影长凳的线性亮度比1.6 / 3.5 / 7
contactDark接触带 / 开放长凳亮度(越低越好)0.75 / 0.42 / 0.18
penumbraMono剖面中从接触点向外单调上升的比例0.6 / 0.8 / 0.95
contactGap离开接触点时的亮度骤降——即物体底部的亮缝(越低越好)0.30 / 0.10 / 0.02

实现层面(metrics.js):contact.p0 → p1定义了一条横穿物体前缘的剖面线,lineProfile()沿该线采样 64 点并平滑;penumbraMono统计剖面中逐点亮度不低于前一点 ×0.985 的比例;contactGap则扫描剖面前 16 点中最大的"离开接触点时的亮度落差",专门检测 peter-panning 的亮缝。对应的渲染侧对策在 SKILL.md:normalBias要按阴影贴图分辨率缩放(4096 用 0.0045、2048 用 0.0085),并在每个物体下方烘焙径向衰减的接触暗池(contact pool,y=0.0012),因为仅靠阴影贴图无法让接缝变暗。

实测:demo 该项 9.57 分——shadowRatio = 6.75、contactDark = 0.12、penumbraMono = 1.0(完全单调)、contactGap = 0(无亮缝)。

4. 体积光柱与灰尘(Volumetric shafts and dust)

暗房间 + 光束 + 灰尘,是这张场景"氛围感"的来源,也是最容易把黑位和木纹一起毁掉的环节。

分数看起来像什么
3没有光束,或一块硬边半透明平面、能看到明显边界(baseline 实测 4.7)
6光束两侧柔和,但亮度从头到尾相同;灰尘要么没有、要么均匀撒布
9沿长度方向调制、逐渐淡入淡出的光束;没有任何硬楔形;光束旁的黑位保持完整;灰尘只在有光处可见(demo 实测 8.0)

测量指标:

子指标说明锚点
shaftDelta光束相对其外平行参考带的亮度级数1.2 / 4 / 9,且不得超过 35——超过说明光束正在"洗掉"房间黑位,70 时该项降到 3 分
shaftRough光衰轮廓的锯齿度(越低越好)0.5 / 0.22 / 0.08
dustFrac光束内(减去暗墙参考区)比其 7 px 邻域亮 6+ 级的柔和光斑占比0.0002 / 0.001 / 0.003

这是评分卡中唯一带cap(上限惩罚)的指标:anchor()函数的cap参数(metrics.js)实现"太亮同样扣分"——shaftDelta超过 35 后分数线性跌到 3。对应渲染侧,SKILL.md 给出的对策是:每块光束平面按世界高度淡出(smoothstep(0.02, 0.34, y))、增益控制在 0.13,并时刻检查黑位p1是否低于 7。实测中shaftDelta = 41.14已经越过 35 上限(得分 7.95),说明该 demo 的光束增益仍偏亮,这是指标直接暴露出的可优化点。

5. 背景光线条带(Background rays)

墙上投射的窗户光条带,是"这扇窗真实存在"的视觉证据。

分数看起来像什么
3没有条带,或只有一条(baseline 实测 0.8)
6两条条带,亮度均匀
9四条或更多条带,宽度与亮度都不相等、沿墙逐渐变暗、位于物体之后、不与物体争夺注意力(demo 实测 8.8)

测量指标:

子指标说明锚点
rayContrast横穿墙带剖面的 p92 − p8,亮度级数1 / 3 / 6.5,上限:超过 45 视为眩光墙(90 时得 3 分)
rayPeaks墙面上可区分的条带数量1 / 2 / 4

rayPeaks的峰值检测在 metrics.js:对 96 点剖面平滑后,统计"比两侧 3 点都高 0.7 级以上"的局部峰。实测 demo 该项 8.75 分:rayContrast = 36.15(未过 45 上限,得 10 分)、rayPeaks = 3 → 7.5——条带数量刚好差一条到满分,是一个明确的量化缺口。

6. 色彩与色调映射(Colour and tone mapping)

暗场景最容易在最后一步色调映射上翻车:高光裁切、黑位抬升、渐变条带、冷暖不分。

分数看起来像什么
3高光裁切成纯白,黑位抬成灰,渐变出现条带,冷暖色混为一谈(baseline 实测 4.5)
6没有裁切,但画面平淡,或对比过压
9窗户高光是柔和的滚降而非裁切,黑点落在 2~7 之间,暖主光对冷阴影,暗墙无条带(demo 实测 9.8)

测量指标(5 个子指标,最多的一项):

子指标说明锚点
clipPct任一通道 ≥ 254 的像素占比(越低越好)4 / 1.2 / 0.25
p1黑点亮度(越低越好,直到被压死)34 / 18 / 7
crushPct亮度 < 2 的像素占比45 / 22 / 8
warmCool同一长凳受光面与阴影面 R−B 之差1 / 8 / 18
flatRun暗墙像素中连续 6+ 个完全相同值(8-bit 条带)的占比(越低越好)0.5 / 0.2 / 0.04

源码中clipPct、crushPct、p1/p50/p99在全帧统计(metrics.js);flatRun通过逐行扫描"同一 RGB 三元组连续 6 像素以上"的运行来检测 8-bit 色带,对应渲染侧在 ACES 之后、sRGB 编码之后加±0.0015哈希抖动(dither 0.003)来消除条带。实测 demo 该项 9.84 分,五项子分数全部在 9.4 以上。

7. 纹理分辨率与纹素密度(Texture resolution and texel density)

木材的真实感最终取决于纹理在 1 px 尺度上是否还有细节、以及掠射视角是否抖动。

分数看起来像什么
3最近视角下是模糊的"软泥",或木板边缘视角出现闪烁摩尔纹(16 px 字段实测 6.9)
6正面看锐利,掠射角"爬行"
9正面 1 px 细节,相机移动半像素时画面平静,源纹素 ≥ 每设备像素 1 个(demo 实测 9.7)

测量指标:

子指标说明锚点
fineStd1 px 拉普拉斯标准差(亮度级)0.5 / 1.4 / 3.2
texelPerPx最细纹理带上,源纹素 / 设备像素(低于 1 表示被放大、发虚)0.45 / 0.9 / 1.6
shimmer视图偏移半像素后的平均差值 /(0.5 × 平均梯度),越低越好3.2 / 1.9 / 1.15;预过滤表面约为 1,走样表面 3+

shimmer是最有技巧性的一个测量:它需要把相机视图平移半像素重渲染一帧(scorecard.mjs 的shimmerRatio()),如果表面经过正确预过滤,两帧差异应与梯度成正比(比值 ≈1);如果走样,半像素移动会产生远超梯度的跳变(3+)。texelPerPx则由 demo 的KiboriScore.texelPerPx()实时计算(demo.html:最细纤维带 230 周期/米 × 纹素/周期,除以每米像素数)。实测 demo 该项 9.69 分,shimmer = 1.13(接近预过滤理想值 1)。

8. 高光与粗糙度真实感(Specular and roughness realism)

这是 demo 中唯一"未达标"的评分项,也是最能体现这套评分卡价值的案例。

分数看起来像什么
3没有高光,或一个白色塑料感的热点(baseline 实测 4.5)
6一道光滑光泽,完全无视木纹
9一道宽阔柔和的光泽横穿木板,遇到开放毛孔处粗糙度上升而被打断,永远不接近纯白(demo 实测 6.5,偏短)

测量指标:

子指标说明锚点
sheenSpread模糊木材线性亮度的 p95 / p251.06 / 1.3 / 1.9
paintClip木材中接近纯白像素的占比(越低越好,"油"而非"塑料漆")0.05 / 0.01 / 0.0005
hlBreakup光泽最亮四分位内仍然存活的纹理(粗糙度贴图打断光泽)0.004 / 0.018 / 0.045

实现上(metrics.js):用 6 px 盒式模糊分离"宽泛光泽"与"木纹高频"(避免把纹理解成高光);hlBreakup只在高亮四分位内部统计高频能量,专门检查粗糙度贴图是否真正打断了光泽。实测 demo 该项仅 6.52 分,拖累项是hlBreakup = 0.01 → 4.29与sheenSpread = 1.32 → 6.11——如 SKILL.md 所记录的,两位"理论上的修复者"(整板清漆、各向异性 0.3)都抬高了数字却让木板更难看了,因此被当场否决。这正是"分数必须对照片负责"的实例。

如何诚实地阅读一张评分卡

rubric.md 用一整节专门警告"分数是会骗人的",并给出四条纪律:

  1. 指标都是代理(proxy)。有两件事任何指标都看不见:物体是否被读成"它本身"(一块匾,而不是一个托盘);光照选择是否有品味。处理办法:把渲染图交给一位没见过这些数字的独立评审人,取两方分数中较低的那个。
  2. 消融(ablation)是对指标本身的检验。scorecard.mjs会逐个关闭渲染层并重测(scorecard.mjs)。如果某个层没有推动任何它应该推动的评分项,或者关掉它反而让总分上升,说明指标量错了东西,或该层没有赚回它的成本。demo 实测中关闭"工作室反射"(env)让总分+0.14(因为它抬高了阴影),这个结果被如实记录(last-scorecard.json 的ablation.env.drop = -0.14),而不是被调参调掉。
  3. 分数只在相同尺寸与 DPR 下可比。demo 在 1440x900 @1x 得 8.8,@2x 得 8.7(更多纹素,但灰尘光斑细于 7 px 测量窗口)。不同分辨率下的分数不能横向比较。
  4. 达标就停。当每项都达到目标,或某项因任务之外的原因进入平台期,就停止迭代。demo 的高光项停在 6.5~7.4:光泽是"宽阔"而非"独立高光",而能制造独立高光的两个杠杆(填满木板的清漆、各向异性)在 2x 裁剪下都会糊掉木纹或抹掉雕刻,于是当场被拒。

把评分卡跑起来:从 baseline 到 ablation

rubric.md 是"打分之眼",而 scorecard.mjs 是"打分之手"。它的工作流完整印证了 rubric 的机制:

# 1. 服务 demo 页面并安装 headless 浏览器 npm i playwright-core # 2. 跑一次完整评分(capture → 冻结时间 → 八项打分 → 逐层消融 → 写 JSON) node references/scorecard.mjs --size 1440x900 --dpr 1 --target 8 # 3. 可选:--sync 把 references/metrics.js 原样同步回 demo.html 的内联块 node references/scorecard.mjs --sync # 4. 可选:--generic 模式把任意页面(提供 regions.json)纳入评分 node references/scorecard.mjs --url URL --generic --selector canvas --regions regions.json

关键点(scorecard.mjs):

  • headless Chromium + 冻结时间:KiboriScore.freeze(true)固定时间与相机摆动(freeze()会把时钟钉在 t=5.2,见 demo.html),否则"指标会随相机移动";
  • 渲染与读取在同一任务内完成:KiboriScore.render(nx, ny)与getImageData在同一次page.evaluate中执行,因此无需preserveDrawingBuffer(绘制缓冲在一次任务后即失效);
  • 固定尺寸与 DPR:--size/--dpr决定可比性;结果写入 JSON,含all_on、baseline、ablation、cost、short与pass字段;
  • 退出码即 CI 信号:任何一项低于--target时进程以退出码 1 结束(pass: false),可直接接进持续集成;
  • shimmer 需要第二帧:grab(0.5, 0)用相机setViewOffset平移半像素重渲染,再与基准帧比较。

实测基线(last-scorecard.json):全部层关闭时总分为3.73(最低项 rays 0.75),全部开启后8.81(最低项 spec 6.52,唯一低于 8 目标的一项)。消融表还揭示了每层的真实贡献:关掉grain总分掉 0.94 且波及 texture/spec;关掉key掉 0.70 且波及 shadow/tone/texture;关掉shafts掉 0.31 且波及 rays;而关掉env反而 +0.14。这些数字正是 rubric"层必须赚回成本"原则的实证。

延伸:把这份 rubric 用到你自己的场景

rubric.md 的可迁移性来自两处刻意设计:

  1. 区域与指标解耦:所有测量都基于regions()返回的帧比例坐标矩形。更换场景时,只需照 demo.html 的regions()模式重写世界点投影(demo 用RL(mesh, ...)把物体局部坐标投影到屏幕),metrics.js与锚点一行都不用动。PROMPT.md 的 Remix 提示正是这么要求的:换题材、换调色、重写regions()、先打 baseline、跑 ablation、迭代到每项 ≥8 或说明平台期原因。
  2. 锚点语义可复用:anchor()的"三锚点 + 线性 + clamp + 可选 cap"机制(metrics.js)不限于木材——任何"从可测量物理量到主观质量分数"的映射都可以套用,包括那些"过高反而扣分"的 cap 型指标(光束亮度、眩光对比度)。

最后提醒:rubric 本身反复强调的"给独立评审人看 + 取较低分",说明再精密的代理指标也无法替代人的判断。用这套评分卡做的是把模糊的"感觉"变成可复现、可消融、可对比的量化证据——分数负责拦截回归,人负责最后的品味把关。

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载

相关推荐

上一篇:终极进程管理工具fkill-cli:跨平台高效管理进程的完整指南
下一篇:Bluestone未来路线图:即将发布的7大令人期待的新功能

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询