首页
/ three.js FlakesTexture 详解:程序化生成汽车漆面闪光颗粒法线贴图

three.js FlakesTexture 详解:程序化生成汽车漆面闪光颗粒法线贴图

2026-09-06 18:57:53作者:范垣楠Rhoda

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 像素的随机半径避免了所有颗粒完全雷同,营造出大小不一的自然感。颗粒总数与 widthheight 无耦合,因此若传入更大的画布尺寸,颗粒密度会随之下降。

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();

这是整个算法的核心:每个颗粒都被赋予一个独立且随机的法线方向。nxny 在 -1~1 间随机取值,而 Z 分量固定为 1.5(大于 X、Y 的最大可能取值),随后通过向量归一化保证法线为单位长度。由于 Z 分量显著大于 X、Y,绝大多数颗粒的法线仍以"朝上"为主,X/Y 方向的扰动又各不相同——每个颗粒因此对光线有各自略为不同的反射角度,这正是金属闪光片随光照角度变化产生闪烁观感的根源。颜色写入时采用法线贴图的标准编码公式:RGB 的 R、G 通道为 nx*127+127ny*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.htmlwebgpu_clearcoat.html(WebGPU 渲染器)实际旋转视角观察闪光随光照的动态变化。
  • 相关 API:包装画布所用的 CanvasTexture、承载法线的 MeshPhysicalMaterial(含 clearcoat / clearcoatRoughness / clearcoatNormalMap 等属性)以及材质法线属性 normalMapnormalScale 是理解完整链条的相邻文档。

综上,FlakesTexture 用约 50 行代码把"汽车金属漆闪光"从美术资源需求变成了一个随处可调用的运行时工具:理解它的画布生成三步算法,再配合 CanvasTextureRepeatWrappinganisotropynormalScaleMeshPhysicalMaterial 的清漆参数,你就能在任意 three.js 场景中快速打造出逼真的金属烤漆质感。

登录后查看全文
热门项目推荐
相关项目推荐