Three.js中为领带模型生成随等级变化的随机无碰撞圆形材质
解决方案:Three.js 动态生成带随机无碰撞圆形的领带材质
方案2:Canvas 动态生成纹理(最直观易实现)
这是落地成本最低的方案,直接通过Canvas绘制随机无碰撞圆形,再转为Three.js纹理,步骤清晰:
- 生成无碰撞圆形数据:编写函数生成指定数量的不重叠圆,核心逻辑是新圆与已有圆的距离需大于两者半径之和。
- Canvas绘制:创建Canvas元素,先绘制领带底色,再逐个绘制随机颜色、尺寸的圆形。
- 转为Three.js纹理:用
THREE.CanvasTexture包装Canvas,直接赋值给材质的map属性;后续更新时重绘Canvas后设置texture.needsUpdate = true即可。
代码示例:
// 生成无碰撞随机圆数据 function generateNonOverlappingCircles(count, canvasSize = 512) { const circles = []; const maxRadius = canvasSize / 8; const minRadius = canvasSize / 32; while (circles.length < count) { const newCircle = { x: Math.random() * canvasSize, y: Math.random() * canvasSize, radius: minRadius + Math.random() * (maxRadius - minRadius), color: `rgb(${Math.floor(Math.random()*255)}, ${Math.floor(Math.random()*255)}, ${Math.floor(Math.random()*255)})` }; // 检查碰撞 let isOverlap = false; for (const circle of circles) { const dx = newCircle.x - circle.x; const dy = newCircle.y - circle.y; const distance = Math.sqrt(dx*dx + dy*dy); if (distance < newCircle.radius + circle.radius) { isOverlap = true; break; } } if (!isOverlap) circles.push(newCircle); } return circles; } // 创建Canvas纹理 function createCircleTexture(userLevel) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = 512; // 建议用2的幂次尺寸 const ctx = canvas.getContext('2d'); // 绘制领带底色 ctx.fillStyle = '#2c3e50'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制圆形 const circles = generateNonOverlappingCircles(Math.min(userLevel, 100)); circles.forEach(circle => { ctx.fillStyle = circle.color; ctx.beginPath(); ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2); ctx.fill(); }); // 转为Three.js纹理 const texture = new THREE.CanvasTexture(canvas); texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping; return texture; } // 应用到领带模型 const tieMaterial = new THREE.MeshBasicMaterial({ map: createCircleTexture(userLevel), side: THREE.DoubleSide }); tieMesh.material = tieMaterial;
方案1:Shader 实时绘制圆形(性能更优,适合大量圆)
当用户等级接近100时,Shader方案性能优于Canvas,利用GPU并行计算处理圆形绘制。核心是将圆数据打包为uniform传入着色器,用GLSL的数学函数替代if判断优化性能。
步骤:
- JS端生成归一化的无碰撞圆数据(位置、半径对应UV坐标[0,1]范围,颜色转为0-1的vec3数组)。
- 将数据作为uniform传入自定义
ShaderMaterial。 - 片元着色器中遍历所有圆,用
step函数判断像素是否在圆内,实现无if的颜色叠加。
代码示例:
JS 部分
// 生成归一化圆数据 function generateNormalizedCircles(count) { const circles = []; const maxRadius = 0.125; const minRadius = 0.03125; while (circles.length < count) { const newCircle = { uv: [Math.random(), Math.random()], radius: minRadius + Math.random() * (maxRadius - minRadius), color: [Math.random(), Math.random(), Math.random()] }; let isOverlap = false; for (const circle of circles) { const dx = newCircle.uv[0] - circle.uv[0]; const dy = newCircle.uv[1] - circle.uv[1]; const distance = Math.sqrt(dx*dx + dy*dy); if (distance < newCircle.radius + circle.radius) { isOverlap = true; break; } } if (!isOverlap) circles.push(newCircle); } // 打包为uniform格式 return { centers: new Float32Array(circles.flatMap(c => c.uv)), radii: new Float32Array(circles.map(c => c.radius)), colors: new Float32Array(circles.flatMap(c => c.color)), count: circles.length }; } // 创建自定义着色器材质 const circleData = generateNormalizedCircles(userLevel); const tieMaterial = new THREE.ShaderMaterial({ uniforms: { uBgColor: { value: new THREE.Color('#2c3e50') }, uCircleCenters: { value: circleData.centers }, uCircleRadii: { value: circleData.radii }, uCircleColors: { value: circleData.colors }, uCircleCount: { value: circleData.count } }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform vec3 uBgColor; uniform vec2 uCircleCenters[100]; // 最大支持100个圆 uniform float uCircleRadii[100]; uniform vec3 uCircleColors[100]; uniform int uCircleCount; varying vec2 vUv; void main() { vec3 finalColor = uBgColor; for (int i = 0; i < 100; i++) { if (i >= uCircleCount) break; // 仅早期跳出循环,性能影响极小 vec2 delta = vUv - uCircleCenters[i]; float distanceSq = dot(delta, delta); float radiusSq = uCircleRadii[i] * uCircleRadii[i]; // 用step实现无if的颜色切换 finalColor = mix(finalColor, uCircleColors[i], step(distanceSq, radiusSq)); } gl_FragColor = vec4(finalColor, 1.0); } `, side: THREE.DoubleSide }); tieMesh.material = tieMaterial;
方案3:Three.js 内置方案?
Three.js没有直接提供“生成随机无碰撞圆形纹理”的内置API,所有实现都基于Canvas或Shader的自定义逻辑。如果尝试用Three.js的2D渲染组件(如Sprite)绘制圆再烘焙纹理,反而不如直接用Canvas高效。
总结
- 快速实现选方案2,代码简单易调试,适配大多数场景;
- 追求高性能(尤其是100个圆的情况)选方案1,GPU并行计算效率更高;
- 无直接内置方案,两种自定义方案可覆盖需求。
内容的提问来源于stack exchange,提问作者Eugene
相关产品推荐
相关产品推荐

