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

基于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:

Core Idea

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.

Step 1: Set Up the Marching Cubes Grid

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;
};
Step 2: Build the Custom ShaderMaterial

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;
Step 3: Sync Metaball Updates

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();
Key Tips & Optimizations
  • 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 06:46:44