Reputation: 13
Title: How to properly quarter-cut spheres in Three.js with clipping planes?
Body:
I'm working on a Three.js project where I'm trying to create a visual of layered spheres, and I want to cut each sphere in a "quarter-cut" style — keeping three quarters of the sphere while removing one quarter, like in these typical cross-section graphics of the Earth's layers.
So far, I’ve been trying to use clipping planes, but I can’t seem to get the right part of the sphere to remain — it either cuts too much or leaves the wrong section. I tried flipping the planes and moving them on different axes, but it didn’t help.
Here’s the current code:
<!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="-10" max="10" 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/[email protected]";
import { OrbitControls } from "https://esm.sh/[email protected]/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; // Enable damping (inertia)
controls.dampingFactor = 0.25; // Damping factor
controls.enableZoom = true; // Enable zooming
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // Soft white light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5).normalize();
scene.add(directionalLight);
// Create a clipping plane
// Angled planes meeting at the "bite" point
const cut = 3;
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, 0xffffff);
const planeHelperY = new THREE.PlaneHelper(planeY, 20, 0xffffff);
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 angle = (i / numSpheres) * Math.PI * 2;
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);
}
}
// Handle slider input
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; // Adjust this value to slow down or speed up the particle movement
// 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>
What I’d like to achieve:
Keep three quarters of the spheres (like a visible cross-section) while cutting along two perpendicular planes.
Adjust the cut depth with a slider.
What’s happening now:
The planes cut the spheres, but I often see the wrong part of the shape remaining.
What am I missing? Is there a better approach to achieve this type of clipping in Three.js? Any guidance would be appreciated!
Upvotes: 1
Views: 24