Three.js平滑粒子开启depthTest时透明度截断问题求助
Three.js 0.173.0 粒子系统深度测试下的透明度叠加问题
我在Three.js 0.173.0中实现了带柔边效果的粒子系统,但开启depthTest后,当粒子因Z排序改变绘制顺序时,粒子的半透明像素会直接覆盖后方场景内容,而非正确叠加。尝试过双渲染通道、调整材质设置等方法,问题仍未解决。
问题表现截图:
相关代码如下:
import * as THREE from "https://esm.sh/three"; let camera, scene, renderer, material, points; const vertexShader = ` attribute float u_id; varying vec2 vUv; varying vec3 vColor; void main() { vec3 newPosition = position; // // THE CIRCLES HAVE TO BE POSITIONED IN THE SHADER // if (u_id == 0.0) { newPosition = vec3(-0.6, 0.0, 1.2); vColor = vec3(1.0, 0.0, 0.0); } if (u_id == 1.0) { newPosition = vec3(0.0, 0.0, 1.0); vColor = vec3(0.0, 1.0, 0.0); } if (u_id == 2.0) { newPosition = vec3(0.6, 0.0, 1.1); vColor = vec3(0.0, 0.0, 1.0); } vUv = newPosition.xy; gl_PointSize = 200.0; // Control the circle size gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); } `; const fragmentShader = ` precision mediump float; varying vec2 vUv; varying vec3 vColor; void main() { vec2 uv = gl_PointCoord - vec2(0.5); // Center the UVs in the circle float dist = length(uv); float alpha = smoothstep(0.5, 0.1, dist); // Smooth fade at edges vec3 color = vColor; //vec3(1.0, 0.0, 0.0); // Red color if (dist > 0.5) discard; // Make the circle smooth gl_FragColor = vec4(color, alpha); } `; const VERTEX_COUNT = 3; // Initial vertex positions are [0, 0, 0] const getInitialVertices = () => { const vertices = new Float32Array(VERTEX_COUNT * 3); return vertices; }; // Generate vertex IDs const getVertexIds = () => { const ids = new Float32Array(VERTEX_COUNT); for (let i = 0; i < VERTEX_COUNT; i++) { ids[i] = i; } return ids; }; function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 10000 ); camera.position.z = 4; scene.add(camera); const uniforms = {}; const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(getInitialVertices(), 3)); geometry.setAttribute("u_id", new THREE.BufferAttribute(getVertexIds(), 1)); material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, transparent: true, // // THE OPACITY CUT OCCURES WHEN DEPTH TEST IS ON // depthTest: true, }); points = new THREE.Points(geometry, material); scene.add(points); renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); } function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } window.addEventListener("load", () => { init(); animate(); });
解决方案
这个问题的核心是深度缓冲与半透明渲染的冲突:开启depthTest时,先绘制的粒子会将深度值写入缓冲,后绘制的粒子即使半透明,只要深度值大于缓冲中的值,就会被完全剔除,无法叠加。结合你的场景(粒子必须在Shader中定位),可以采用以下两种方案:
方案1:关闭深度写入,保留深度测试
修改材质设置,关闭depthWrite,这样粒子会参与深度测试(不会出现在不应该出现的后方物体前面),但不会写入深度缓冲,允许后续半透明粒子叠加:
material = new THREE.ShaderMaterial({ // ... 其他设置 transparent: true, depthTest: true, depthWrite: false, // 新增这一行 });
注意:这种方法需要确保场景中不透明物体先于粒子渲染,否则粒子可能会覆盖不透明物体。
方案2:按Z轴从远到近排序粒子
由于你在Shader中固定了粒子位置,可以手动计算每个粒子的视空间深度,对几何体的顶点进行排序,确保远的粒子先绘制,近的后绘制:
- 在初始化时,计算每个粒子的世界位置,转换为视空间深度:
// 在init函数中,创建points之后 const positions = [ new THREE.Vector3(-0.6, 0.0, 1.2), new THREE.Vector3(0.0, 0.0, 1.0), new THREE.Vector3(0.6, 0.0, 1.1) ]; // 计算视空间深度并排序 const sortedIds = positions.map((pos, idx) => ({ idx, depth: camera.position.distanceTo(pos) })).sort((a, b) => b.depth - a.depth) // 远的在前 .map(item => item.idx); // 重新排序几何体的u_id属性 const newIds = new Float32Array(VERTEX_COUNT); sortedIds.forEach((originalIdx, newIdx) => { newIds[newIdx] = originalIdx; }); geometry.setAttribute("u_id", new THREE.BufferAttribute(newIds, 1));
这种方法能保证半透明粒子按正确顺序叠加,同时保留深度测试和深度写入,但如果粒子位置动态变化,需要每一帧重新排序。
额外优化:移除discard语句
你的片段Shader中if (dist > 0.5) discard;可以去掉,因为smoothstep(0.5, 0.1, dist)在dist>0.5时alpha已经是0,透明像素会自动被处理,不需要手动丢弃,避免影响深度缓冲的行为。
内容的提问来源于stack exchange,提问作者David Szucs
相关产品推荐
相关产品推荐

