本节摘要:纹理把二维图像"贴"到三维表面,着色器决定图像如何参与颜色计算;采样是两者的交汇点——着色器按插值出的纹理坐标去图像上取色。本节讲清采样机理、三类高频纹理通道的分工,以及从内置材质到 NodeMaterial 再到 ShaderMaterial 的定制梯度。承接 3.1 与 3.2 的通道体系,是材质体系的收口。
"采样"听起来抽象,动作其实具体:每个顶点带一对 0 到 1 的纹理坐标(2.3 节的 UV 属性),三角形内部任意像素的坐标由三个顶点插值而来,着色器拿这对坐标去图像上"查表"取出颜色。图像是二维数组,坐标是小数,落在四个像素之间怎么办——这正是采样器的职责:插值方式、越界策略、缩小放大用什么算法,都属于采样状态。
三类高频纹理通道,各回答一个独立问题:
// 通道一:颜色纹理——"表面是什么颜色" const base = new BABYLON.Texture("brick.png", scene); mat.diffuseTexture = base; base.uScale = 4; // 平铺四遍:贴图坐标乘系数,砖墙不拉伸 base.vScale = 4; base.wrapU = BABYLON.Texture.WRAP_ADDRESSMODE; // 越界后平铺而不是截断 // 通道二:法线纹理——"表面有什么凹凸" const bump = new BABYLON.Texture("brick_normal.png", scene); mat.bumpTexture = bump; mat.invertNormalMapY = true; // 不同建模工具的法线方向约定不同,凹凸反了就切这个开关 // 通道三:反射与环境纹理——"周围映在表面什么样子" // 3.2 节的环境贴图就是这一族,此处不再重复配置
法线纹理值得多说一句:它不改变几何,只欺骗光照计算——像素的"朝向"从贴图里读出来,凹凸感由明暗对比伪造。这决定了它的性价比(三角形数不变)与边界(侧看会露馅,轮廓始终是平的)。远看砖墙用贴图,近看要摸得动砖缝,才值得上真几何。
采样状态里有个高频坑:贴图在近处清晰、远处闪烁成噪点,是缩小采样算法在"硬取一个点"。各向异性过滤就是解药:
// 远处闪烁的三行诊断与修复 base.trilinearSampling = true; // 三线性插值:基础平滑 base.anisotropicFilteringLevel = 8; // 各向异性级别:斜视角清晰度 // 引擎层面也可以一次设满:scene.defaultMaterial... // 级别 4 到 8 是画质与带宽的平衡点,移动端别超过 4
内置材质的通道是固定配方,两件事会逼你走出内置:一是效果特殊(扫描线、卡通描边、溶解消失),二是性能苛刻(砍掉一切用不到的通道)。两条路线按"会不会写着色器语言"分流。
NodeMaterial 用可视化节点编辑器搭材质——像素怎么算,图上怎么连。适合不写着色器代码的美术与快速原型。ShaderMaterial 则直接写着色器程序,每个像素执行一遍的两个函数:顶点阶段定位置、片元阶段定颜色:
// ShaderMaterial 最小骨架:一个"按距离渐变成透明"的演示 const shader = new BABYLON.ShaderMaterial("fade", scene, { vertexSource: BABYLON.Effect.ShadersStore.vertexShader, // 复用引擎内置顶点着色器 fragmentSource: ` precision highp float; varying vec3 vPosition; // 顶点阶段传来的插值数据 uniform float uTime; // 每帧更新的外部参数 void main(void) { float fade = 0.5 + 0.5 * sin(vPosition.y * 3.0 + uTime); gl_FragColor = vec4(vPosition.x + 0.5, fade, 0.6, 1.0); // 输出 RGBA 颜色 }`, }, { attributes: ["position"], uniforms: ["uTime"] }); shader.onBindObservable.add(() => { shader.setFloat("uTime", performance.now() / 1000); // 每帧把时间喂给着色器 }); ball.material = shader;
对着色器先建立三个认知,比会写更重要:其一,它是逐像素执行的程序,你写的每一行乘法每秒可能发生上亿次,代码里一个多余的纹理采样就足以压垮移动端帧率;其二,uniform 是每帧从 JavaScript 传入的常量,varying 是从顶点插值到像素的数据,这两条通道是 CPU 与 GPU 的分界线;其三,着色器首次使用要编译,运行时新材质会造成 1.2 节说的帧率尖刺,加载期预热能治。
三条路的选型一句话说清:内置材质改参数能解决的不上节点,节点能连出来的不写着色器,写着色器的场景里每个像素都该有存在的理由。
⚠️ 常见坑:法线纹理与颜色纹理的坐标共享同一套 UV,平铺系数跟着材质走而不是跟着纹理走;两张贴图想各自平铺不同的遍数,要配独立的坐标偏移而不是指望属性互不干扰。
贴图为什么看起来"糊了一层灰"? 先查颜色空间。颜色贴图按伽马空间存储,PBR 的计算却在线性空间进行,进出两道转换由纹理的颜色空间开关控制;开关配错,明暗关系整体漂移,之后怎么调参数都救不回来。这个开关是纹理问题排查的第一站,比怀疑贴图本身便宜得多。
同一张贴图平铺后接缝明显怎么办? 分两种:贴图本身左右边缘不连续,是内容问题,回美术重做可平铺版本;接缝轻微错位则是采样精度问题,贴图尺寸用 2 的幂(256、512、1024)再把各向异性开高一点,两条都做基本能消干净。
颜色体系到此完整:通道、物理参数、纹理、着色器四层全部打通。但真实感还差最后一块拼图——影子。下一节揭开它"第二遍渲染"的本质。