You need to enable JavaScript to run this app. Three.js THREE.Points 如何实现 sprite sheet 纹理偏移显示不同粒子效果
优惠活动
大模型
产品
解决方案
定价
更多

Three.js THREE.Points 如何实现 sprite sheet 纹理偏移显示不同粒子效果

基于ShaderMaterial的Sprite Sheet点粒子实现方案

核心原理

原生THREE.PointsMaterial仅支持单张全图采样,无法为单个顶点传入自定义UV偏移参数。通过自定义ShaderMaterial实现时,核心逻辑是:为每个点粒子绑定独有的子图UV起点属性,在片元着色器中将点精灵的本地UV映射到Sprite Sheet的对应子区域完成采样。

前置参数定义

首先需要明确Sprite Sheet的基础配置:

  • 大纹理的横向子图数量cols、纵向子图数量rows
  • 单个子图在大纹理上的UV占比为 1/cols(横向)、1/rows(纵向)
  • 每个子图的UV起点计算需适配WebGL的UV坐标系(原点在左下角):
    • 横向起点:u = (frameIndex % cols) / cols
    • 纵向起点:v = 1 - Math.floor(frameIndex / cols)/rows - 1/rows

纹理加载时需额外配置:

  • 关闭边缘采样溢出:设置texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping
  • 匹配UV方向:根据纹理加载的flipY设置,调整v坐标的计算逻辑避免画面颠倒

注意: 纹理采样时需预留0.001量级的UV边距,避免因为纹理压缩、浮点精度问题采样到相邻子图的像素。

自定义顶点属性配置

在BufferGeometry中为每个点粒子添加帧偏移属性,记录该点要采样的子图UV起点:

const particleCount = 200;
const geometry = new THREE.BufferGeometry();
// 基础位置属性
const positions = new Float32Array(particleCount * 3);
// 自定义帧偏移属性,存储每个点的子图UV起点
const frameOffsets = new Float32Array(particleCount * 2);

const cols = 4;
const rows = 2;
const totalFrames = cols * rows;

for (let i = 0; i < particleCount; i++) {
  // 填充随机位置
  positions[i * 3] = (Math.random() - 0.5) * 30;
  positions[i * 3 + 1] = (Math.random() - 0.5) * 30;
  positions[i * 3 + 2] = (Math.random() - 0.5) * 30;

  // 随机分配子图索引
  const frameIdx = Math.floor(Math.random() * totalFrames);
  const u = (frameIdx % cols) / cols;
  const v = 1 - Math.floor(frameIdx / cols) / rows - 1 / rows;
  frameOffsets[i * 2] = u;
  frameOffsets[i * 2 + 1] = v;
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('aFrameOffset', new THREE.BufferAttribute(frameOffsets, 2));

ShaderMaterial代码实现

顶点着色器

负责接收自定义的帧偏移属性,传递给片元着色器,同时按照透视关系计算点精灵的屏幕尺寸:

attribute vec2 aFrameOffset;
varying vec2 vFrameOffset;
varying vec2 vLocalUv;

void main() {
  vFrameOffset = aFrameOffset;
  // 点精灵本地UV范围为0-1
  vLocalUv = uv;

  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  // 按照透视距离缩放点大小,和原生PointsMaterial逻辑对齐
  gl_PointSize = 180.0 * (1.0 / -mvPosition.z);
  gl_Position = projectionMatrix * mvPosition;
}

片元着色器

负责将点精灵的本地UV映射到Sprite Sheet的对应子区域,完成纹理采样:

uniform sampler2D uSpriteSheet;
uniform float uColCount;
uniform float uRowCount;
varying vec2 vFrameOffset;
varying vec2 vLocalUv;

void main() {
  // 映射到子图的实际采样坐标,加微小边距避免采样到相邻帧
  float sampleU = vFrameOffset.x + vLocalUv.x / uColCount + 0.001;
  float sampleV = vFrameOffset.y + vLocalUv.y / uRowCount + 0.001;

  vec4 texColor = texture2D(uSpriteSheet, vec2(sampleU, sampleV));
  // 丢弃透明像素,避免出现方形遮挡
  if (texColor.a < 0.1) discard;

  gl_FragColor = texColor;
}

材质初始化

const material = new THREE.ShaderMaterial({
  uniforms: {
    uSpriteSheet: { value: loadedSpriteTexture },
    uColCount: { value: 4 },
    uRowCount: { value: 2 }
  },
  vertexShader: `这里替换为上面的顶点着色器代码`,
  fragmentShader: `这里替换为上面的片元着色器代码`,
  transparent: true,
  depthWrite: false
});

const points = new THREE.Points(geometry, material);
scene.add(points);

常见问题排查

  • 画面出现相邻子图的杂色:确认纹理的wrap模式设置为ClampToEdgeWrapping,检查UV计算时是否添加了足够的边距偏移,避免浮点精度导致的采样越界
  • 采样的子图方向颠倒:调整纹理的flipY属性配置,同步修改v坐标的计算公式,让坐标方向和实际纹理方向匹配
  • 所有点显示同一个子图:检查aFrameOffset属性的填充逻辑,确认每个顶点的属性值没有被统一覆盖
  • 点精灵尺寸过大/过小:调整顶点着色器中gl_PointSize的计算系数,匹配实际项目的相机距离和点大小需求

内容的提问来源于stack exchange,提问作者multitijmes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.29 10:36:04