基于Three.js与Marching Cubes实现指定颜色的渐变多色元球
Absolutely, you can recreate that stylized metaball effect with Three.js and Marching Cubes—with full control over assigning explicit colors from your given array to each metaball. Let’s break down how to pull this off, focusing on integrating custom shaders with Three.js’s Marching Cubes implementation:
The Vimeo example uses a shader-only distance field approach, but with Marching Cubes, we’ll generate a tangible mesh from the metaball distance field, then use a custom ShaderMaterial to sample each metaball’s influence and map it to your color array. The key is syncing metaball data (position, radius, color index) between your JavaScript logic and the shader, then calculating which metaball has the strongest influence on each fragment to apply the right color.
First, initialize Three.js’s MarchingCubes class to generate the metaball mesh. This class requires a distance field function that returns the signed distance from any point to the closest metaball (or combined field):
const resolution = 32; // Adjust for detail vs performance const marchingCubes = new THREE.MarchingCubes( resolution, new THREE.MeshBasicMaterial(), // We'll replace this later true, // Use smooth shading true // Enable auto-update ); // Define your metaball data (position, radius, color index) const metaballs = [ { pos: new THREE.Vector3(-2, 0, 0), radius: 1.5, colorIdx: 0 }, { pos: new THREE.Vector3(2, 0, 0), radius: 1.5, colorIdx: 1 }, // Add more metaballs as needed ]; // Assign the distance field function to Marching Cubes marchingCubes.position.set(0, 0, 0); marchingCubes.scale.set(6, 6, 6); marchingCubes.updateDistanceField = function (position) { let dist = Infinity; // Calculate combined distance field for all metaballs metaballs.forEach(ball => { const d = position.distanceTo(ball.pos) - ball.radius; dist = Math.min(dist, d); }); return dist; };
Now create a ShaderMaterial that receives metaball data as uniforms, then in the fragment shader, determine which metaball influences each pixel most and apply the corresponding color from your array.
Uniforms Definition
First, define the uniforms to pass metaball data to the shader:
const colorArray = [ new THREE.Color(0xff0000), // Red for first metaball new THREE.Color(0x00ff00), // Green for second new THREE.Color(0x0000ff) // Blue for third ]; const uniforms = { metaballPositions: { value: metaballs.map(b => b.pos) }, metaballRadii: { value: metaballs.map(b => b.radius) }, metaballColors: { value: colorArray.map(c => c.toArray()) }, cameraPosition: { value: camera.position } };
Vertex Shader
Pass the world position of each vertex to the fragment shader:
varying vec3 vWorldPos; void main() { vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
Fragment Shader
Calculate the closest metaball to each fragment, then sample its color:
varying vec3 vWorldPos; uniform vec3 metaballPositions[]; uniform float metaballRadii[]; uniform vec3 metaballColors[]; uniform vec3 cameraPosition; void main() { float closestDist = 9999.0; int closestIndex = 0; // Loop through all metaballs to find the closest one for (int i = 0; i < 10; i++) { // Adjust loop length to match your metaball count if (i >= metaballPositions.length) break; float dist = distance(vWorldPos, metaballPositions[i]) - metaballRadii[i]; if (dist < closestDist) { closestDist = dist; closestIndex = i; } } // Apply the color of the closest metaball gl_FragColor = vec4(metaballColors[closestIndex], 1.0); // Optional: Add subtle shading based on normal (calculate from distance field) vec3 normal = normalize(vec3( dFdx(closestDist), dFdy(closestDist), 1.0 )); float lighting = dot(normal, normalize(cameraPosition - vWorldPos)); gl_FragColor.rgb *= 0.5 + 0.5 * lighting; }
Assign the Material to Marching Cubes
Replace the default material with your custom shader:
const shaderMaterial = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: document.getElementById('vertexShader').textContent, fragmentShader: document.getElementById('fragmentShader').textContent, side: THREE.DoubleSide }); marchingCubes.material = shaderMaterial;
If you animate or modify metaballs (positions, radii), update the uniforms to reflect changes:
// Example: Animate a metaball's position function animate() { requestAnimationFrame(animate); metaballs[0].pos.x = Math.sin(Date.now() * 0.001) * 2; // Update uniforms uniforms.metaballPositions.value = metaballs.map(b => b.pos); marchingCubes.update(); // Trigger Marching Cubes to regenerate the mesh renderer.render(scene, camera); } animate();
- Performance: Marching Cubes resolution and metaball count directly impact performance. For more metaballs, consider spatial partitioning (like a grid) to reduce the number of distance calculations per fragment.
- Color Blending: If you want smooth color transitions between overlapping metaballs, calculate a weighted average of colors based on each metaball’s influence (instead of just picking the closest one).
- WebGL Array Limits: WebGL has limits on uniform array sizes. If you need many metaballs, use a texture to store metaball data instead of uniform arrays (this is more scalable).
内容的提问来源于stack exchange,提问作者compmonks

