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
相关产品推荐
相关产品推荐

