3、粒子纹理:加载纹理贴图、圆形渐变纹理、自定义纹理制作

纹理这东西,说白了就是粒子的「皮肤」。没有纹理的粒子,就像没穿衣服的小人——能看,但总觉得少了点什么。我个人习惯把纹理看作是粒子系统的灵魂,选对了纹理,整个效果立马就活了。

今天咱们就聊聊三种常用的纹理加载方式:直接加载图片、用 Canvas 生成圆形渐变、以及完全自定义纹理。嗯,这三种方式我在项目里都踩过坑,一个一个说。

3.1 加载纹理贴图

最直接的方式,就是用 TextureLoader 加载一张现成的图片。比如星星、光晕、雪花,网上找一张 PNG 透明底图就行。

const loader = new THREE.TextureLoader();
const texture = loader.load('path/to/star.png');

const material = new THREE.PointsMaterial({
    size: 0.5,
    map: texture,
    transparent: true,
    blending: THREE.AdditiveBlending,
    depthWrite: false
});

这里有个细节:transparent: true 必须开,否则透明背景会变成白色方块。我刚开始做的时候忘了这茬,结果满屏白点,还以为是图片坏了……

注意: 纹理图片建议用 2 的幂次方尺寸,比如 64x64、128x128、256x256。非 2 的幂次方虽然也能用,但 GPU 处理时会多一步缩放,性能会打折扣。

另外,AdditiveBlending 是粒子特效的标配。它会让重叠的粒子颜色叠加变亮,产生发光效果。你想想看,星星、火花、烟雾,哪个不是越叠越亮?

3.2 圆形渐变纹理

很多时候我们不需要外部的图片,用 Canvas 画一个圆形渐变纹理就够了。这样做的好处是:不依赖外部资源,加载快,而且可以动态调整。

function createCircleTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 64;
    canvas.height = 64;

    const ctx = canvas.getContext('2d');
    const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
    gradient.addColorStop(0, 'rgba(255,255,255,1)');
    gradient.addColorStop(0.5, 'rgba(255,255,255,0.8)');
    gradient.addColorStop(1, 'rgba(255,255,255,0)');

    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 64, 64);

    return new THREE.CanvasTexture(canvas);
}

const texture = createCircleTexture();

这段代码的核心是 createRadialGradient。从圆心到边缘,颜色从白到透明渐变。边缘羽化得越柔和,粒子看起来就越自然。

小技巧: 如果你想要粒子边缘更锐利,可以把 addColorStop(0.5, ...) 改成 addColorStop(0.8, ...)。反之,想要更柔和,就把 0.5 改成 0.2。我在做星云效果时,就喜欢用 0.3 左右的渐变,看起来像一团朦胧的气体。

为什么会用 Canvas 而不是直接加载图片?说白了,Canvas 纹理是「活」的。你可以在运行时修改渐变参数,生成不同大小、不同透明度的纹理。这在动态粒子系统里特别有用。

3.3 自定义纹理制作

圆形渐变虽然好用,但有时候我们需要更复杂的纹理。比如带尾巴的彗星、带棱角的雪花、或者不规则的光斑。这时候就得自己动手画了。

我记得有一次做粒子爆炸特效,需要每个粒子在爆炸瞬间有一个「拖尾」效果。圆形渐变显然不行,得画一个长条形的纹理,一端亮一端暗。

function createCustomTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 128;
    canvas.height = 128;

    const ctx = canvas.getContext('2d');

    // 画一个带尾巴的圆形
    ctx.beginPath();
    ctx.arc(64, 64, 20, 0, Math.PI * 2);
    ctx.fillStyle = 'white';
    ctx.fill();

    // 画尾巴
    ctx.beginPath();
    ctx.moveTo(64, 64);
    ctx.lineTo(120, 80);
    ctx.lineTo(110, 100);
    ctx.closePath();
    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.fill();

    return new THREE.CanvasTexture(canvas);
}

你看,这里我画了一个圆,然后在右边加了一个半透明的三角形尾巴。实际渲染时,粒子会带着这个尾巴旋转,看起来就像在运动。

核心思路: 自定义纹理的本质就是「用 Canvas 画任意形状」。你可以画星形、画箭头、画字母,甚至画一个复杂的图案。只要记住:纹理的透明区域要处理好,否则粒子之间会互相遮挡,看起来脏脏的。

嗯,这里要注意一点:Canvas 纹理的尺寸不要太大。128x128 已经够用了,再大就浪费 GPU 带宽。我曾经为了追求细节,画了 512x512 的纹理,结果粒子数量一多,帧率直接掉到 20。后来改成 64x64,效果几乎没差别,帧率却回到了 60。

3.4 三种纹理的对比

类型 优点 缺点 适用场景
加载图片 效果丰富,可直接用设计稿 依赖外部资源,加载慢 固定效果的粒子(如雪花、星星)
圆形渐变 轻量、无外部依赖、可动态调整 形状单一,只有圆形 光晕、烟雾、通用粒子
自定义纹理 形状任意,灵活度高 需要手写 Canvas 代码 特殊效果(拖尾、箭头、不规则形状)

我个人建议:能用圆形渐变就别用图片,能用自定义就别用圆形渐变。说白了,越「轻」的方案,性能越好,维护成本也越低。但如果你需要特别复杂的纹理,比如带渐变色的星云图案,那还是老老实实加载图片吧。

避坑指南: 我曾经在移动端项目里用了大量 256x256 的纹理图片,结果 iPhone 6 直接卡成幻灯片。后来把所有纹理压缩到 64x64,并用 Canvas 生成圆形渐变替代了大部分图片,性能才恢复正常。所以,纹理尺寸和数量一定要根据目标设备来权衡。

好了,纹理这块就聊这么多。下一节咱们会把这些纹理真正用到粒子系统里,看看它们在不同场景下的表现。你想想看,同样的粒子,换上不同的纹理,效果能差出十万八千里——这就是纹理的魅力。