Appearance
天穹之境
背景:模型站在深灰色
clearColor前,像博物馆展品——功能都有了,但"不在世界里"。 过程:环境系统 V1 完成——程序化天空(Canvas 渐变贴图)、地面三模式、粒子系统、风场、体积云 V1、雾效、状态持久化。
在环境系统出现之前,MikuMikuAR 的天空是一块黑板。
准确地说,是 clearColor —— 一种深灰色,r=0.1, g=0.1, b=0.1。模型站在这块灰布前面,像博物馆里打了光的展品。好看,但不像在世界里。
scene.ts 从不觉得这是个问题。它管模型加载,管 VMD 播放,管物理演算,管相机视角。天空?天空不就是背景吗?背景不就是纯色吗?纯色有什么好聊的。
直到有一天,用户说:
"加个天空吧。"
一、天空的七种死法
"天空有什么难的?" scene.ts 一开始是这么想的。
Babylon.js 有 SkyMaterial 啊 —— 开箱即用,大气散射,太阳位置,统统给你算好。一行 import,一行 new,赋值给 scene.skybox,齐活。
第一死:SkyMaterial 不接受简单的颜色参数。它要的是 turbidity、rayleigh、luminance、mieCoefficient、mieDirectionalG —— 五个气象学参数,每个都像咒语。用户要的是"把天空调亮一点",你给他五个滑块,他问你 turbidity 是啥。
"那换渐变," scene.ts 说,"三色渐变,顶、中、底,用户自己拉,直观。"
第二死:Babylon.js 没有内置渐变天空材质。
"自己写 shader," babylon-mmd 在旁边淡淡地说,"不难,顶点着色器传 UV,片元按 V 值插值三个颜色。"
scene.ts 觉得有道理。它写了一个 procSky.fx 文件 —— 标准的 ShaderMaterial,顶点着色器把 uv 传给片元,片元用 mix(mix(topColor, midColor, v*2), botColor, (v-0.5)*2) 做三段渐变。配上 backFaceCulling = false,贴在一个大球体上,朝内渲染。
第三死:球体的 UV 和它想的不一样。
scene.ts 以为 V=0 是顶部,V=1 是底部。结果渲染出来 —— 上面是地面色,下面是天空色。整个世界倒过来了。
"球体 UV," babylon-mmd 又开口了,语气还是那么平,"建模软件的惯例是 V=0 在底部,V=1 在顶部。你按 V 从上到下插值,就是从地到天,反的。"
scene.ts 把渐变方向反过来了。好,天是天,地是地了。
第四死:ShaderMaterial 不支持 onBindObservable 在每一帧更新 uniform。或者说,它支持,但 scene.onBeforeRenderObservable 里改 material.setColor3("skyColorTop", color) 会在下一帧才生效。用户拉滑块,天空慢半拍变。
"延迟感," 用户说,"不舒服。"
第五死:自定义 shader 要处理的东西太多了。深度写入?不要。背面剔除?反的。雾效?要自己在 shader 里乘。alpha?不用。每一项都是坑,踩一个记一个。
scene.ts 有点烦了。不就是个渐变天空吗?怎么这么多事。
二、画布之天
"为什么一定要用 shader?" app.css 从旁边路过,随口说了一句。
scene.ts 抬头:"不然呢?天空是 3D 的,不用 shader 用什么?"
"用贴图啊," app.css 说,"你画一张渐变图,贴到球体内侧不就行了?渐变图还不容易?Canvas 2D 画一下,几行的事。"
scene.ts 愣住了。
对啊。它一直在想"3D 天空要用 3D 的办法",但渐变天空本质上就是一张贴图。既然是贴图,为什么不能用 2D 的办法画好了再贴上去?
它连夜改写了方案:
- 创建一个 1×256 的 Canvas(宽 1 像素,高 256 像素)
- 用 Canvas 2D 的
createLinearGradient画三段渐变 —— 上、中、下三个颜色节点 - 把 Canvas 转成
DynamicTexture - 用
StandardMaterial,把这张贴图赋给emissiveTexture(自发光,不受光照影响) - 贴在球体上,朝内渲染
六行代码。没有自定义 shader。没有 uniform 更新延迟。没有雾效兼容问题。
用户拖动"天空顶部颜色"滑块 —— scene.ts 在 Canvas 上重画渐变 —— DynamicTexture.update() —— 天空立即变色。零延迟。
scene.ts 看着屏幕上平滑过渡的天空,半天说不出话。
它以前觉得"高级"等于"好"。shader 比贴图高级,所以用 shader。但渐变天空这个场景里,Canvas 2D + StandardMaterial 比自定义 shader 更稳、更快、更简单。
有时候最笨的办法,就是最好的办法。
三、地面与风
天空搞定了,地面也跟着活了。
以前的地面就是一块灰色平面,GroundMesh,给个 StandardMaterial,颜色设成深灰。够用,但 boring。
环境系统里,地面有了三种模式:
- 纯色 —— 老样子,深灰一块
- 网格 ——
GridMaterial,白色的线在深色地面上,像工程图纸的坐标纸。用户能调主色、线色、间距 - 棋盘格 —— 黑白格,像国际象棋盘,测比例用的
scene.ts 给每种模式都做了切换逻辑。选哪个,哪个就显示,另外两个藏起来。简单。
然后是粒子。
粒子系统是 Babylon.js 的老本行。ParticleSystem 加 PointParticleEmitter,设置发射速率、速度范围、生命周期、颜色渐变 —— 一套组合拳下来,樱花、雪花、雨滴、烟花,各有各的样子。
但粒子有个问题 —— 它们是直上直下的。
樱花不会垂直下落。雪也不会。它们会被风吹歪。
"那就加个风," scene.ts 说。
风不是实体。风是一种力 —— 它没有形状,没有位置,只有方向和强度。它作用于所有粒子,让它们在下落的同时往旁边飘。
scene.ts 加了三个参数:风速 X、风速 Y、风速 Z。默认 X 是正的 —— 风从左往右吹。樱花粒子在重力作用下下落的同时,被风往 X 正方向推,划出一道斜线。
像真的一样。
四、云和雾
云是后来加的。
一开始 scene.ts 觉得云是"高级功能",V1 不用做。但天空做好之后,总觉得少点什么 —— 纯色的天太空了。像一张没有云的照片,干净,但不真实。
"体积云 V1," 它给自己定了目标,"不用太复杂,一层就行。"
它用了一排 CloudMaterial 的平面。不,不对 —— Babylon.js 没有云材质。它用的是半透明的平面,贴上噪声纹理,缓慢移动。从地面往上看,像是一层薄薄的低云。
云的高度可调。用户拉滑块,云层就升高或降低。速度也可调。快慢自己定。
很简单。但有了云和没有云,天空的感觉完全不一样。
最后是雾。
雾是最容易被低估的东西。没有雾的时候,远处的地面和天空在无穷远的地方相接,边界锐利得像刀切的。加一点雾 —— scene.fogMode = FOGMODE_EXP2,scene.fogDensity = 0.01 —— 远处的地面渐渐融到天空里,边界模糊了,世界突然就深了。
像真的有空气一样。
五、持久化
环境系统做完之后,scene.ts 本来以为就完事了。
直到 app.go 走过来问:"这些参数,用户关了重开,还在吗?"
scene.ts 愣了一下。
不在。全在内存里。页面一刷新,天空变回深灰,地面变回纯色,粒子关掉,风停下,云散了,雾也没了。
"这不行," app.go 说,"加进 config.json 里。用户调好了,下次打开还得是那样。"
于是 app.go 在 Config 结构体里加了一个 Env 字段,类型是 EnvState。EnvState 里面又套了六个小结构体 —— 天空、地面、粒子、风、云、雾。每个结构体里是对应的参数。
加一个 Go binding:SetEnvState。前端调它,它把状态写进 config.json。
前端启动时,从 GetConfig() 里把 env 读出来,逐项恢复到场景里。
这样用户调好的天空颜色、地面样式、粒子种类、风的强度、云的高度、雾的浓度 —— 关掉软件再打开,还在。
像有人帮你记住了你喜欢的天气。
六、穹顶
环境系统全部做完那天,scene.ts 站在场景中央,抬头看。
头顶是渐变的天空 —— 顶部深蓝,中部浅蓝,靠近地平线的地方泛着一点暖黄。脚下是网格地面,白色的线向远方延伸,消失在薄雾里。风从左边吹来,粉色的樱花斜斜地飘落,云层在高处缓慢移动。
模型站在中间,裙摆随着物理摆动,头发一荡一荡。
scene.ts 以前觉得"天空不就是背景吗"。现在它明白了 —— 背景不是空的。背景是世界的容器。没有容器,模型只是飘在虚空中的展品。有了天空、地面、风、云、雾,模型才像是站在一个地方。
聚合者的工作,不只是把模型搬上台。还要给它们一个能站得住的世界。
MenuStack 从旁边路过,看到它在发呆,问:"想什么呢?"
"想下一个该做什么," scene.ts 说,"环境有了,模型有了,动作有了,音乐有了。还有什么?"
MenuStack 想了想:"软件管理吧。用户的工具都散在桌面上,每次找 Blender 还要切出去。你说,要是我们能把它们都收进来……"
scene.ts 没说话。它抬头看着天空中缓慢移动的云,心里隐隐觉得 —— 这个世界的边界,还在往外扩。
教训:天空不是背景,是世界的容器。最「高级」的办法不一定是最好的——Canvas 2D 渐变贴图比自定义 shader 更稳、更快、更简单。要学会用笨办法解决真问题。