着色像水彩不像CG:平涂两三档、背光掺冷影、纸纹±3%、颜料边、柔边假投影
自写轻量着色器,不用three的写实材质:只取家族色板,光是平涂不是高光;不开实时阴影、不用PBR,金属、玻璃、塑料、发光材质一概不引。
现场跑 WebGL画的两张纸卡(示例)。拖动或聚焦后按方向键转它,静止时一帧不画。切「水彩」「CG」看同一个物件两种画法;水彩下面六个开关一样一样拆开看。
平涂三档
背光冷影
纸纹±3%
颜料边
假投影
入夜
数值 / 代码 色板 纸面 #FFFDFA / #F6F5F1 · 纸边 #E4E0D6 · 墨 #3E3C35 · 冷影海湾蓝 #6E8A93
光 漫反射量化成两三档平涂,没有镜面高光、没有反射
冷影 背光面掺一点海湾蓝(同门厅垂柳「后层掺海湾蓝」)
纸纹 着色器里程序化纸纤维,亮度 ±3%,不新增贴图
颜料边 每个面靠边深一圈(同「洇一层纸」转场外缘那圈)
投影 物件下一块柔边椭圆 rgba(74,77,53,.22),运行时画,不开实时阴影
入夜 跟墙壁开关与系统深色,整体压暗偏冷(程序化的纸 .30 / .33 / .45)
用在哪 小屋工具页四件物件(paper.ts);刷刷纸闪卡照搬这一套(纸色v3.11.0起对齐界面上的半透明白卡);岗岗案头手册(颜色全走--book-*令牌,读不到就不画);plan台历(现状:侧栏改深色后,2026-09-22由假投影改成three阴影贴图的真投影)。card的镭射是全场唯一「亮」的东西:一条随倾角滑过卡面的软带,只加不乘,加完照样压平涂、纸纹、颜料边。
为什么 物件和门厅那幅水彩画、洇纸转场说同一种话:它们是纸做的文具,不是产品渲染图。card的卡身是唯一的例外色(深蓝哑光#182B3D),因为全息闪卡本来就不是纸做的,拿纸色去涂只会得到一张「纸卡」;这两支色只归card用。
坑 颜色不走three的色彩管理:贴图NoColorSpace、输出原样sRGB,和页面里那张图逐值一致,乘法都在sRGB上做。屋主的画贴上去也照加纸纹(台历那三张界面图不加,那是真实界面),否则贴了画的纸和旁边没贴的纸像两种材料。
出处:Ysevan_web/docs/xiaowu-ui-spec.md「6.9.2 着色:像水彩,不像 CG」(「只取家族色板」「唯一的例外是 card」「光是平涂」「纸纹」「颜料边」「假投影」「入夜」「镭射」)、「4.9.2 接管:接缝为什么看不出来」(「不走 three 的色彩管理」);tools/shua/design-proposal.md:252「同一种纸」;tools/gang/design-proposal.md:427「着色器自写」、:428「令牌读不到就不画」;tools/plan/docs/3d-decor-proposal.md:71「真投影」
两件物件交接用屏幕门抖动:按4×4 Bayer阈值丢片元,不用半透明混合
同一块画布上的两件物件没法各给各的CSS透明度。着色器里每件一个uFade,按4×4 Bayer阈值丢片元;相邻两件阈值互补,叠在一起时各占各的像素;uFade=1时一个片元都不丢。
现场跑 WebGL画的两件纸物件(示例):一叠纸卡交接给一座台历。拖进度(或点「交接一遍」);切到「半透明混合」看排序问题,关掉「阈值互补」看缝;右边放大镜把画布中间一块放大4倍看网点,像素比切到1更明显。
数值 / 代码 // 片元着色器(本页演示的写法):每件物件一个 uFade(= 这一层的 present)
float bayer2(vec2 a) { a = floor(a); return fract(a.x * .5 + a.y * a.y * .75); }
float bayer4(vec2 a) { return bayer2(.5 * a) * .25 + bayer2(a); } // 4×4,0..15/16
float th = bayer4(gl_FragCoord.xy) + 1. / 32.;
if (uFadeFlip > .5) th = 1. - th; // 相邻两件阈值互补(按层的单双)
if (uFade < th) discard; // uFade = 1:一个都不丢,停留姿态逐像素不变
// 投影本来就是半透明的一层:alpha 直接乘 uFade;深度测试照常
用在哪 小屋工具页:四件物件挂在同一块sticky舞台的画布上,相邻两件交接时同时看得见,各用自己的相机取景、后一层画在前一层上面。
为什么 不用半透明混合,就不用给翻动的纸页排序,深度测试照常;停留时uFade=1,画面和不抖动时逐像素相同。要真透明得给两件各开一块离屏缓冲再合成,多两块画布大小的显存(1440×900、像素比1.75时带4倍多重采样约110MB,不带约25MB但交接时边缘有锯齿)。屋主2026-09-20看过实机后裁定:抖动就这样,不做离屏合成。
坑 抖动是一格格的像素:2倍屏上读作一层细纹理,1倍屏、尤其夜里浅色物件压在深底上看得出网点。它只出现在交接那几十svh里,停留时没有——那是已知代价,不是遗留缺陷,别因为「网点看得见」把这条重开。阈值不互补时两件抢同一批像素:一半像素两件都画(深度定输赢),另一半谁都不画、露出底色。
出处:Ysevan_web/docs/xiaowu-ui-spec.md「6.9.3 滚动编排」(「屏幕门抖动」「4×4 Bayer」「uFadeFlip」「投影本来就是半透明的一层」「已知取舍」「110 MB」「抖动就这样,不做离屏合成」「一个渲染器、一块画布」)
静态海报就是3D终态那一帧:用脚本滚到停留姿态截出来,不挑好看的中间帧
访客在3D准备好之前一路滚过去,那一格一直是海报;3D只在看不见时接管。海报要是和终态不是同一帧,换成3D的那一刻姿态就会跳。
现场跑 WebGL画一张翻一圈、落到3/4角的纸卡(示例)。「截一张海报」把选中的那一帧压成WebP放进槽位;「让3D接管」把槽位换成活的3D终态,右边逐像素对比两者差多少、差在哪。换成「好看的中间帧」再截一次,看会跳多少。
槽位
差异:越红差越多
通道差>8的像素 —
最大差 —
平均差 —
数值 / 代码 海报 尺寸960×640(3:2),透明底 WebP,每张 ≤40KB;.poster 带 aspect-ratio: 3 / 2,图没到也按3:2占位
截法 scripts/render-tool-posters.mjs:Node 自带 WebSocket 走 Chrome 调试协议,打开本机生产预览,
滚到停留姿态,轮询 --rail-progress 直到停住并核对等于目标(不相等就报错、不写图),
按 .visual 的矩形截图,sharp 转 WebP;生产代码里不为它留任何开关或调试路由
重截 改了造型、着色、时间线或停留姿态都要重截;--only shua / gang / card 只重截一张
实测 视口1440×900、2倍像素比、同一块991×660设备像素:通道差 >8的像素0.89%、最大差53、平均0.60
(换图前截在进度0.6的那张:5.01%、最大差228)
用在哪 小屋工具页刷刷、岗岗、card三张海报;减弱动效、窄屏、低端机看到的就是它,取代了原来的虚线占位块。
为什么 3D是叠加层,静态版自己就得是完整的一页。最后一件不退场,访客先滚过去再回来时停的就是海报,所以海报必须等于终态。海报是截出来的、不是画的:造型和着色只有一份,换了画面重跑脚本就行。
坑 最后一件的每个动作都要在停留结束前落定,并留一截静止(card留5.6svh,门禁要求≥5),海报截的就是这一截。岗岗第一版海报截在进度0.6(第二页正翻着),和终态对不上,换掉了。截图前不等进度停住也不会报错,所以脚本要核对进度等于目标。
出处:Ysevan_web/docs/xiaowu-ui-spec.md「6.9.4 静态版」(「海报 960×640」「最后一层的海报 = 3D 终态」「不许挑好看的中间帧」「v1 是在进度 0.6 截的」「通道差 >8 的像素 0.89%」「海报是截出来的,不是画的」「核对它等于目标」)、「6.9.3 滚动编排」(「最后一件的每个动作」)
停稳重画:走动时按小预算画,停稳后排下一帧按屏幕像素画满一次
软件渲染精简档里,走进小屋走动时绘制缓冲封在60万像素;停稳在墙角后排下一帧用325万(屋主那台按DPR 1.25画满)重画一次,之后一帧不画。这一帧不进看门狗采样。
现场跑 WebGL现画的一幅示例水彩小画(不是屋主的画)。拖动它、点「走两步」或聚焦后按方向键就是「走动」:按屋主那台走动时的像素比0.537画,明显发糊;停下后下一帧按这块屏的像素比重画一次,右边记下这一帧多久、之后又画了几帧。打开「反例:不分档」看停下来也一直糊。
数值 / 代码 // Ysevan_web/components/hall-walk/quality.ts
export const softMaxPixels = 600_000; // 走动:画被放大到远超源图,多画的像素买不到细节
export const softRestPixels = 3_250_000; // 停稳:(2040×1.25)×(1019×1.25) = 3,248,062.5,向上取整
// 岛:循环要停的那一刻(heading === 0 && !wants)排下一帧调 scene.resize(restDensity, true)
// 改密度、改尺寸、重画在同一个同步任务里做完,合成器看不到中间状态(不闪)
// 重新起步时 kick() 先 resize(density, false) 换回走动那一档;这一帧清 lastRenderAt,不进看门狗
用在哪 小屋走进小屋的精简档(屋主那台没显卡的台式机走的就是它)。
为什么 墙角每幅画在屏幕上只有465 CSS宽,比源图636还小;走动预算只给它画250个像素,而门厅第一屏那张<img>都有396——走进去之后比走进去之前更糊,同一页里就有对照组。墙角完全静止,rAF当场退出,所以停稳重画是一次性开销(2026-09-24实测约18ms,readPixels强制同步量);走动中一直保持ratio 1.0是98.6ms一帧,补不回来。验收判据因此换成「走进去之后那幅画的清晰度不得低于门厅第一屏里同一幅画」。
小屋走进去之后糊的很,页面都看不清
坑 精简档不让灯呼吸:呼吸是停下来之后唯一还在动的东西,留着它render()永远返回true,夜里根本不存在「停稳」,重画没机会发生——这是读代码发现的,不是验收。量这一帧耗时别用gl.finish():Chrome 153上它不等画完(只读到1.8–2.0ms),紧接着的readPixels还要等17–28ms。验「不闪」要真交出一帧空画面:只改尺寸、隔200ms才画,Chrome会沿用上一帧,判据红不了。
出处:Ysevan_web/docs/xiaowu-ui-spec.md「4.9.8 精简档:走动一档预算,停稳另一档」(「小屋走进去之后糊的很」「比源图的 636 还小」「验收判据」「98.6ms」「17.8 / 18.8 / 18.6ms」「finish() 在这台 Chrome(153)上不等画完」「排下一帧」「不会闪」「精简档不让灯呼吸」);Ysevan_web/components/hall-walk/quality.ts「softMaxPixels」、「softRestPixels」(「3,248,062.5」)