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

如何在Three.js中使用裁剪平面正确对球体进行四分之一切割?

如何在Three.js中使用裁剪平面正确对球体进行四分之一切割?

嘿,我看了你的代码和需求,问题主要出在裁剪平面的方向和参数设置上,咱们一步步来把它搞定~

问题根源:裁剪平面的方向搞反了

Three.js的裁剪平面是基于平面法向量和平面到原点的距离来工作的,而且默认会保留满足 plane.distanceToPoint(顶点) >= 0 的几何体部分。另外,clipIntersection 默认是 false,意味着会保留任意一个裁剪平面的保留侧(也就是去掉同时不满足所有平面条件的四分之一区域)——这正是你要的四分之三球体效果,但你的平面方向设置反了,导致切掉了错误的部分。

你的原代码里,裁剪平面的法向量是(-1,0,0)和(0,-1,0),这会保留x≤-cut和y≤-cut的区域,切掉的是x>-cut且y>-cut的部分,和你想要的截面方向正好相反。

解决方案:调整裁剪平面的设置

我们需要把法向量改成(1,0,0)和(0,1,0),同时调整constant参数,让平面位于你想要的切割位置:

  1. 平面法向量指向你想保留的一侧(比如x正方向、y正方向)
  2. constant参数设为-cut,这样平面方程就是x = cut和y = cut
  3. 滑块控制时,把constant设为-depth,让滑块值直接对应切割深度

修改后的关键代码部分

首先是裁剪平面的初始化:

// 初始切割深度设为0(切到球体中心)
const cut = 0;
// 调整平面法向量和constant,保留x >= cut 或 y >= cut的部分(去掉x<cut且y<cut的四分之一)
const planeX = new THREE.Plane(new THREE.Vector3(1, 0, 0), -cut);
const planeY = new THREE.Plane(new THREE.Vector3(0, 1, 0), -cut);

然后是滑块的事件处理:

// Handle slider input
slider.addEventListener('input', function() {
    const depth = parseFloat(this.value);
    // 把constant设为-depth,对应平面x=depth和y=depth
    planeX.constant = -depth;
    planeY.constant = -depth;
});

完整修改后的代码

下面是调整后的完整代码,测试一下:当你把滑块调到0时,会切到球体中心,完美露出内部的分层截面;调大正数时,切割变浅,去掉的区域变小;调负数时,切割方向反转~

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Orbital</title>
    <style>
        #slider, #rotationSlider {
            position: absolute;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
        }
        #rotationSlider {
            top: 40px;
        }
    </style>
</head>
<body>
    <input type="range" id="slider" min="-8" max="8" value="0" step="0.1">
    <input type="range" id="rotationSlider" min="0" max="0.1" value="0.01" step="0.001">
    <script type="module">
        import * as THREE from "https://esm.sh/three@0.152.2";
        import { OrbitControls } from "https://esm.sh/three@0.152.2/examples/jsm/controls/OrbitControls.js";

        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 30;

        const renderer = new THREE.WebGLRenderer();
        renderer.localClippingEnabled = true;
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.25;
        controls.enableZoom = true;

        const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
        scene.add(ambientLight);

        const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
        directionalLight.position.set(5, 5, 5).normalize();
        scene.add(directionalLight);

        // 修正后的裁剪平面设置
        const cut = 0;
        const planeX = new THREE.Plane(new THREE.Vector3(1, 0, 0), -cut);
        const planeY = new THREE.Plane(new THREE.Vector3(0, 1, 0), -cut);
        const planeHelperX = new THREE.PlaneHelper(planeX, 20, 0xff0000);
        const planeHelperY = new THREE.PlaneHelper(planeY, 20, 0x00ff00);
        scene.add(planeHelperX);
        scene.add(planeHelperY);
        
        // Add additional spheres
        const numSpheres = 6;
        const radius = 3;
        const spheres = [];
        for (let i = 0; i < numSpheres; i++) {
            const colors = [0xff0000, 0xff3b3b, 0xff6b6b, 0xff9292, 0xffbdbd, 0xfffafa];

            const material = new THREE.MeshPhongMaterial({
                color: colors[i],
                side: THREE.DoubleSide,
                wireframe: false,
                transparent: false,
                opacity: 1-(i/numSpheres),
                clippingPlanes: [ planeX, planeY ],
                clipShadows: true
            });
            const geometry = new THREE.SphereGeometry(radius + i, 64, 64);
            const sphere = new THREE.Mesh(geometry, material);
            sphere.position.set(0, 0, 0);
            spheres.push(sphere);
            scene.add(sphere);
        }

        // Create particles as small spheres
        const particleCountX = 150;
        const particleCountY = 150;
        const particleGeometry = new THREE.SphereGeometry(0.05, 8, 8);
        const particleMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, transparent:true, opacity:0.2 });
        const particlesMatrix = [];
        const particleAngles = [];
        for (let j = 0; j < particleCountX; j++) {
            particlesMatrix[j] = [];
            for (let k = 0; k < particleCountY; k++) {
                const particle = new THREE.Mesh(particleGeometry, particleMaterial);
                particlesMatrix[j][k] = particle;
                particleAngles.push({ angleX: (j / particleCountX) * Math.PI * 2, angleY: (k / particleCountY) * Math.PI * 2 });
                scene.add(particle);
            }
        }

        // 修正后的滑块控制
        slider.addEventListener('input', function() {
            const depth = parseFloat(this.value);
            planeX.constant = -depth;
            planeY.constant = -depth;
        });

        const rotationSlider = document.getElementById('rotationSlider');
        let rotationSpeed = parseFloat(rotationSlider.value);
        rotationSlider.addEventListener('input', function() {
            rotationSpeed = parseFloat(this.value);
        });

        let clock = new THREE.Clock();
        let timeScale = 10;

        // Animation loop
        function animate() {
            requestAnimationFrame(animate);

            let deltaTime = clock.getDelta() * timeScale;

            // Update particles to follow spheres
            for (let j = 0; j < particleCountX; j++) {
                for (let k = 0; k < particleCountY; k++) {
                    const particle = particlesMatrix[j][k];
                    const sphere = spheres[(j + k) % spheres.length];
                    const angles = particleAngles[j * particleCountY + k];
                    angles.angleX += rotationSpeed * deltaTime;
                    angles.angleY += rotationSpeed * deltaTime;
                    const x = sphere.position.x + (radius + (j + k) % numSpheres) * Math.cos(angles.angleX) * Math.sin(angles.angleY);
                    const y = sphere.position.y + (radius + (j + k) % numSpheres) * Math.sin(angles.angleX) * Math.sin(angles.angleY);
                    const z = sphere.position.z + (radius + (j + k) % numSpheres) * Math.cos(angles.angleY);
                    particle.position.set(x, y, z);
                }
            }

            // Rotate spheres
            spheres.forEach(sphere => {
                sphere.rotation.y += rotationSpeed * deltaTime;
            });

            controls.update();
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

额外小调整

我还把滑块的max改成了8(因为你最大的球体半径是3+5=8),这样滑块调到最大值时,平面刚好切到最外层球体的表面,不会出现无效的裁剪;另外把两个平面助手的颜色改成了红和绿,方便你区分两个平面的位置。

现在你拖动滑块就能完美调整切割深度,看到你想要的四分之一切割效果啦~

备注:内容来源于stack exchange,提问作者Simon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:50:27