如何基于二维控制点用Three.js与顶点着色器实现三次贝塞尔曲线?
问题分析与修正方案
核心错误点
- 几何体顶点错误:原代码直接用四个控制点创建Line,这只会画出连接控制点的直线,而非贝塞尔曲线。需要生成一系列从
t=0到t=1的采样点作为几何体顶点。 - t参数误用:着色器中用
position.x作为贝塞尔公式的t值,但t必须是0~1的均匀变量,而非顶点的x坐标。 - 渲染器未设置尺寸:没有给WebGLRenderer设置画布大小,导致渲染结果显示异常。
修正后的完整代码
<script src="https://threejs.org/build/three.js"></script> <script type="module"> const rendSize = new THREE.Vector2(window.innerWidth, window.innerHeight); var scene, camera, renderer, shaderMat; function main() { renderer = new THREE.WebGLRenderer(); renderer.setSize(rendSize.x, rendSize.y); document.body.appendChild(renderer.domElement); scene = new THREE.Scene(); // 调整正交相机参数适配窗口 const aspect = rendSize.x / rendSize.y; camera = new THREE.OrthographicCamera(-10 * aspect, 10 * aspect, 10, -10, -10.0, 10.0); const vShader = ` vec3 toBezier(float t, vec3 P0, vec3 P1, vec3 P2, vec3 P3) { float t2 = t * t; float t3 = t2 * t; float one_minus_t = 1.0 - t; float one_minus_t2 = one_minus_t * one_minus_t; float one_minus_t3 = one_minus_t2 * one_minus_t; return P0 * one_minus_t3 + P1 * 3.0 * t * one_minus_t2 + P2 * 3.0 * t2 * one_minus_t + P3 * t3; } uniform vec3 p0; uniform vec3 p1; uniform vec3 p2; uniform vec3 p3; attribute float t; // 新增t属性,传递0~1的采样值 void main(void) { vec3 pos = toBezier(t, p0, p1, p2, p3); gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } `; generateCurve(vShader); renderer.clear(); renderer.render(scene, camera); } function generateCurve(vShader) { const ptosControle = [ new THREE.Vector3(0.0, 0.0, 0.0), new THREE.Vector3(-0.5, Math.sin(Math.PI/2.0)/2.0, 0.0), new THREE.Vector3(0.5, Math.sin(3.0*Math.PI/2.0)/2.0, 0.0), new THREE.Vector3(1.0, 1.0, 0.0) ]; // 生成贝塞尔曲线的采样点数据:创建100个t值从0到1的顶点 const sampleCount = 100; const positions = []; const tValues = []; for(let i=0; i<=sampleCount; i++){ const t = i / sampleCount; // 临时位置可以随便填,实际会被着色器替换,这里用原点占位 positions.push(0, 0, 0); tValues.push(t); } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute('t', new THREE.Float32BufferAttribute(tValues, 1)); // 添加t属性 shaderMat = new THREE.ShaderMaterial({ uniforms: { p0: { value: ptosControle[0]}, p1: { value: ptosControle[1]}, p2: { value: ptosControle[2]}, p3: { value: ptosControle[3]}, }, vertexShader: vShader, }); const bezierCurve = new THREE.Line(geometry, shaderMat); scene.add(bezierCurve); // 可选:添加控制点连线,方便对比 const controlLine = new THREE.Line( new THREE.BufferGeometry().setFromPoints(ptosControle), new THREE.LineBasicMaterial({color: 0xff0000}) ); scene.add(controlLine); } main(); // 窗口大小变化时更新相机和渲染器 window.addEventListener('resize', () => { rendSize.set(window.innerWidth, window.innerHeight); renderer.setSize(rendSize.x, rendSize.y); const aspect = rendSize.x / rendSize.y; camera.left = -10 * aspect; camera.right = 10 * aspect; camera.updateProjectionMatrix(); renderer.render(scene, camera); }); </script>
修改说明
- 生成采样顶点:创建100个均匀分布的
t值(0到1),作为几何体的顶点属性传递给着色器,确保曲线足够平滑。 - 新增
t属性:在着色器中添加attribute float t,用这个标准化的t值代入贝塞尔公式,替代原代码中错误使用的position.x。 - 优化贝塞尔公式:原公式中
P3的系数为t2*t,虽然计算结果一致,但明确写成t3更符合三次贝塞尔公式的标准形式。 - 设置渲染器尺寸:给
renderer设置窗口大小,并添加窗口 resize 事件监听,保证画面适配窗口变化。 - 可选控制点连线:添加红色的控制点连线,便于直观对比贝塞尔曲线与控制点的位置关系。
内容的提问来源于stack exchange,提问作者Jonas
相关产品推荐
相关产品推荐

