You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js平滑粒子开启depthTest时透明度截断问题求助

Three.js 0.173.0 粒子系统深度测试下的透明度叠加问题

我在Three.js 0.173.0中实现了带柔边效果的粒子系统,但开启depthTest后,当粒子因Z排序改变绘制顺序时,粒子的半透明像素会直接覆盖后方场景内容,而非正确叠加。尝试过双渲染通道、调整材质设置等方法,问题仍未解决。

问题表现截图:
threejs shader opacity cut off

相关代码如下:

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中固定了粒子位置,可以手动计算每个粒子的视空间深度,对几何体的顶点进行排序,确保远的粒子先绘制,近的后绘制:

  1. 在初始化时,计算每个粒子的世界位置,转换为视空间深度:
// 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 00:39:55