three.js FlakesTexture 详解:程序化生成汽车漆面闪光颗粒法线贴图
FlakesTexture 是 three.js 内置的一个轻量级纹理工具类,它通过 Canvas 2D 随机绘制数千个圆形金属颗粒,实时生成一张可在运行时反复复用的"闪光片"贴图。本文以 FlakesTexture 官方文档 为核心,结合其源码 examples/jsm/textures/FlakesTexture.js 与官方示例 webgl_materials_physical_clearcoat.html 中的真实用法,讲清它的工作原理、构造参数、与 CanvasTexture 的搭配方式,以及如何在 MeshPhysicalMaterial 中组合出汽车清漆(clearcoat)般的金属闪光效果。
什么是 FlakesTexture
官方文档对 FlakesTexture 的定位非常明确:
Utility class for generating a flakes texture image. This image might be used as a normal map to produce a car paint like effect.
即:FlakesTexture 是一个用于生成闪光片(flakes)纹理图像的实用工具类,生成的图像可被用作法线贴图,用来模拟类似汽车烤漆的闪光质感。它不是一个独立的 three.js 纹理对象,而是一个纯程序化的"贴图工厂"——构造函数返回的是浏览器原生的 HTMLCanvasElement(Canvas 画布),而不是 Texture。
这类效果在真实世界中对应金属漆面中悬浮的微小金属薄片:它们在光照下会随视角变化产生细碎的明暗闪烁。传统做法需要美术人员预先在 Photoshop 等工具中绘制或用噪波生成器烘焙一张金属闪光法线贴图;而 FlakesTexture 的意义在于——无需任何外部图片资源,完全靠算法在运行时即时生成,且每次生成结果因内部使用了随机数而各不相同。
引入方式:Addon 显式导入
FlakesTexture 属于 three.js 的 Addon(附加组件)体系,不会被打包进核心构建产物中,必须显式导入。文档给出的导入语句为:
import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js';
在仓库中它同样注册于 Addons 的统一导出清单 examples/jsm/Addons.js 内(与 textures/FlakesTexture.js 路径对应),因此你也可以在支持 tree-shaking 的打包环境下从 Addons 索引按需引入,不会把未用到的模块带入最终包体。
需要特别说明的是:这类工具类的构造函数依赖浏览器 DOM API(document.createElement('canvas'))与 Canvas 2D 上下文,因此只能在浏览器环境运行,无法在 Node.js 等服务端渲染环境中直接执行。
构造函数与参数解析
文档对构造器的说明如下:
new FlakesTexture( width : number, height : number ) : HTMLCanvasElement
生成一张新的闪光片纹理图像,返回一个可直接作为 CanvasTexture 输入的画布。
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
| width | number | 生成图像的宽度(像素) | 512 |
| height | number | 生成图像的高度(像素) | 512 |
返回类型:HTMLCanvasElement(生成的画布图像)。
两个参数都可省略,省略时均取默认值 512(512×512 是兼顾清晰度与纹理内存开销的常规尺寸)。对应的完整实现位于 examples/jsm/textures/FlakesTexture.js,构造函数签名与文档完全一致:
class FlakesTexture {
constructor( width = 512, height = 512 ) {
const canvas = document.createElement( 'canvas' );
canvas.width = width;
canvas.height = height;
const context = canvas.getContext( '2d' );
context.fillStyle = 'rgb(127,127,255)';
context.fillRect( 0, 0, width, height );
for ( let i = 0; i < 4000; i ++ ) {
const x = Math.random() * width;
const y = Math.random() * height;
const r = Math.random() * 3 + 3;
let nx = Math.random() * 2 - 1;
let ny = Math.random() * 2 - 1;
let nz = 1.5;
const l = Math.sqrt( nx * nx + ny * ny + nz * nz );
nx /= l; ny /= l; nz /= l;
context.fillStyle = 'rgb(' + ( nx * 127 + 127 ) + ',' + ( ny * 127 + 127 ) + ',' + ( nz * 255 ) + ')';
context.beginPath();
context.arc( x, y, r, 0, Math.PI * 2 );
context.fill();
}
return canvas;
}
}
export { FlakesTexture };
算法原理:从源码逐行解读
从源码结构可以看到,该工具类把整张"闪光片贴图"的绘制拆解为三个清晰阶段。理解每一阶段的作用,有助于你在使用时判断效果偏差该从何处调整。
1. 底色填充——微偏蓝的中性法线基底(对应源码第 24-25 行)
context.fillStyle = 'rgb(127,127,255)';
context.fillRect( 0, 0, width, height );
创建画布后先用 rgb(127,127,255) 铺满整张底图。这张图像是作为法线贴图被消费的,而法线贴图把法线向量编码在 RGB 通道中:R、G 通道分量经 (n*127+127) 换算后归一到 0~255 范围,其中 127 对应法线分量为 0,即表面朝向基准法线(默认垂直表面向外);B 通道对应 Z 轴。rgb(127,127,255) 即基准法线 (0, 0, 1)——一个完全平整、朝上的表面。也就是说底色保证了大面积区域的平滑基底。
2. 铺撒 4000 个颗粒——随机位置与半径(对应源码第 27-31 行)
for ( let i = 0; i < 4000; i ++ ) {
const x = Math.random() * width;
const y = Math.random() * height;
const r = Math.random() * 3 + 3;
循环固定撒 4000 个闪光颗粒:圆心 (x, y) 在整张画布范围内均匀随机分布,圆半径 r 落在 3 ~ 6 像素之间(Math.random()*3 取 0~3,再加 3)。颗粒的稀疏/密集程度由 4000 这个固定迭代次数与半径范围共同决定,尺寸并非单一值——3~6 像素的随机半径避免了所有颗粒完全雷同,营造出大小不一的自然感。颗粒总数与 width、height 无耦合,因此若传入更大的画布尺寸,颗粒密度会随之下降。
3. 随机扰动法线并写入颜色(对应源码第 33-44 行)
let nx = Math.random() * 2 - 1;
let ny = Math.random() * 2 - 1;
let nz = 1.5;
const l = Math.sqrt( nx * nx + ny * ny + nz * nz );
nx /= l; ny /= l; nz /= l;
context.fillStyle = 'rgb(' + ( nx * 127 + 127 ) + ',' + ( ny * 127 + 127 ) + ',' + ( nz * 255 ) + ')';
context.beginPath();
context.arc( x, y, r, 0, Math.PI * 2 );
context.fill();
这是整个算法的核心:每个颗粒都被赋予一个独立且随机的法线方向。nx、ny 在 -1~1 间随机取值,而 Z 分量固定为 1.5(大于 X、Y 的最大可能取值),随后通过向量归一化保证法线为单位长度。由于 Z 分量显著大于 X、Y,绝大多数颗粒的法线仍以"朝上"为主,X/Y 方向的扰动又各不相同——每个颗粒因此对光线有各自略为不同的反射角度,这正是金属闪光片随光照角度变化产生闪烁观感的根源。颜色写入时采用法线贴图的标准编码公式:RGB 的 R、G 通道为 nx*127+127、ny*127+127(把 -1~1 映射到 0~255),而 B 通道(对应归一化后的 Z,理论最大值不超过 255)直接用 nz*255 编码。颗粒以圆形 arc 落笔,形成一个个带方向扰动的圆点闪光单元。
与 CanvasTexture 组合成 three.js 纹理
文档特别强调:FlakesTexture 返回的是画布而不是纹理,需要进一步包装成 CanvasTexture 才能挂到材质上:
const texture = new THREE.CanvasTexture( new FlakesTexture() );
CanvasTexture 是 three.js 内置纹理类,专门用于把 canvas 元素作为纹理源,且实现了 Canvas 内容发生变更后自动触发纹理更新(needsUpdate)的机制,让程序化生成的图像能无缝接入材质系统。
官方示例 webgl_materials_physical_clearcoat.html 展示了从生成到上材质的完整链条。示例中名为"car paint(汽车烤漆)"的球体演示了一段极具参考价值的组合代码:
const normalMap3 = new THREE.CanvasTexture( new FlakesTexture() );
normalMap3.wrapS = THREE.RepeatWrapping;
normalMap3.wrapT = THREE.RepeatWrapping;
normalMap3.repeat.x = 10;
normalMap3.repeat.y = 6;
normalMap3.anisotropy = 16;
// car paint
let material = new THREE.MeshPhysicalMaterial( {
clearcoat: 1.0,
clearcoatRoughness: 0.1,
metalness: 0.9,
roughness: 0.5,
color: 0x0000ff,
normalMap: normalMap3,
normalScale: new THREE.Vector2( 0.15, 0.15 )
} );
这段代码蕴含了几个让"汽车漆面"质感成立的关键实践:
- 重复与平铺:
wrapS/wrapT设为RepeatWrapping,配合repeat分别设为(10, 6)。闪光颗粒按 512×512 平铺后尺度远小于球体,法线细节会被压缩到高频率,视觉上更接近细腻的金属粉末。 - 各向异性过滤:
anisotropy = 16用于在掠射视角下抑制重复平铺产生的模糊与摩尔纹,保证颗粒锐利。 - 法线强度控制:
normalScale取很小的(0.15, 0.15)。这是最容易被忽略的一步——如果按 1:1 强度把颗粒法线全部贴上去,表面会像粗糙砂纸而非平滑漆面。压到 15% 后,颗粒的随机扰动只对光照产生轻微抖动,金属感与平滑感兼得。 - 材质配合:高
clearcoat(清漆层光泽)、低clearcoatRoughness、高metalness、中等roughness与高饱和color的组合,让闪光片位于金属基底之上、清漆层之下,形成"表层光滑透亮、内层微光闪烁"的层次。
值得说明的是,在该官方示例中 FlakesTexture 生成的贴图被赋给了 normalMap 而非 clearcoatNormalMap——即闪光是金属基底本身的性质。同一仓库的 webgl_tsl_clearcoat.html(TSL 版清漆示例)与 webgpu_clearcoat.html(WebGPU 渲染器版本)也以相同的 new THREE.CanvasTexture( new FlakesTexture() ) 方式复用了这个工具,说明它跨 WebGL / WebGPU 两条渲染管线都通用。你也可以反向尝试:把它赋给 clearcoatNormalMap 并在 clearcoatNormalScale 中相应调弱强度,从而让闪光"浮"在清漆层表面——两种语义可根据所需漆面效果取舍。
一套可直接运行的完整范例
下面把上面所有要点整合为一个可直接运行的最小场景:生成一张 1024×1024 的高清闪光贴图,平铺到带清漆的蓝色金属球上,并加入轨道控制器以便转动视角观察闪光随光照的变化。该范例基于 ES Module 写法,需要像官方示例那样通过支持 importmap 的现代浏览器环境运行。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>three.js FlakesTexture 汽车漆面演示</title>
<style>
body { margin: 0; overflow: hidden; background: #111; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "../../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js';
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild( renderer.domElement );
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 1, 4 );
new OrbitControls( camera, renderer.domElement );
// 生成 1024x1024 的闪光片画布,并包装为 CanvasTexture
const flakes = new THREE.CanvasTexture( new FlakesTexture( 1024, 1024 ) );
flakes.wrapS = THREE.RepeatWrapping;
flakes.wrapT = THREE.RepeatWrapping;
flakes.repeat.set( 8, 8 );
flakes.anisotropy = renderer.capabilities.getMaxAnisotropy();
// 汽车漆面材质:清漆 + 金属 + 弱化后的闪光法线
const material = new THREE.MeshPhysicalMaterial( {
color: 0x0033cc,
metalness: 0.9,
roughness: 0.4,
clearcoat: 1.0,
clearcoatRoughness: 0.05,
normalMap: flakes,
normalScale: new THREE.Vector2( 0.15, 0.15 )
} );
const sphere = new THREE.Mesh( new THREE.SphereGeometry( 1, 64, 32 ), material );
scene.add( sphere );
// 用环境贴图让清漆与闪光有可反射的光源
THREE.RoomEnvironment ? createRoomEnvironment() : null;
function createRoomEnvironment() {
import( 'three/addons/environments/RoomEnvironment.js' ).then( ( { RoomEnvironment } ) => {
const pmrem = new THREE.PMREMGenerator( renderer );
scene.environment = pmrem.fromScene( new RoomEnvironment(), 0.04 ).texture;
} );
}
scene.add( new THREE.AmbientLight( 0xffffff, 0.4 ) );
const dir = new THREE.DirectionalLight( 0xffffff, 2 );
dir.position.set( 2, 3, 2 );
scene.add( dir );
renderer.setAnimationLoop( () => renderer.render( scene, camera ) );
</script>
</body>
</html>
几个可以随手调节的效果旋钮:
- 颗粒密度与大小:想更密的闪光就把源码中
4000调大、r的范围调小;想更粗犷的金属片效果则相反。也可以把width/height从 512 提到 1024 以获得更细颗粒。 - 扰动强度:调整
nz(如改为 0.8~2.0)会改变颗粒法线相对表面的倾斜幅度,nz越小单个颗粒反光越"跳"。 - 整体强弱:不必改源码,直接调
normalScale即可连续控制闪光可见度——官方示例证明0.15级别是一个较好的起点,数值超过 1 时颗粒会显得过分凹凸。
注意事项与延伸阅读
- 运行环境限制:构造过程使用 Canvas 2D API,仅在浏览器端可用;且每次调用都会重新执行 4000 次随机绘制,若在初始化阶段一次性生成后缓存纹理即可避免重复开销。由于算法依赖
Math.random(),每次生成的结果都不同,如需跨会话的确定性效果可自行替换为固定种子的随机源。 - 内存与纹理尺寸:默认 512×512 的 RGBA 贴图内存开销很小,可按需提升分辨率;配合
RepeatWrapping时建议同步开启 anisotropy 过滤以获得高质量掠射表现。 - 官方可运行示例:可对照 webgl_materials_physical_clearcoat.html(WebGL 渲染器、含"car paint"球体)、webgl_tsl_clearcoat.html 与 webgpu_clearcoat.html(WebGPU 渲染器)实际旋转视角观察闪光随光照的动态变化。
- 相关 API:包装画布所用的 CanvasTexture、承载法线的 MeshPhysicalMaterial(含
clearcoat/clearcoatRoughness/clearcoatNormalMap等属性)以及材质法线属性normalMap、normalScale是理解完整链条的相邻文档。
综上,FlakesTexture 用约 50 行代码把"汽车金属漆闪光"从美术资源需求变成了一个随处可调用的运行时工具:理解它的画布生成三步算法,再配合 CanvasTexture、RepeatWrapping、anisotropy、normalScale 与 MeshPhysicalMaterial 的清漆参数,你就能在任意 three.js 场景中快速打造出逼真的金属烤漆质感。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00