Skip to content

12. 如何画各种曲线

Published:

有时候我们需要在 3D 场景中画一些曲线。

比如太阳系行星的轨道:

image.png

地图上的飞线:

image.png

或者这种曲线:

image.png

这种线怎么画呢?

这就要用 Three.js 提供的曲线的 API 了。

image.png

我们来试一下。

创建项目:

mkdir curve
cd curve
npm init -y

image.png

安装下 ts 类型包:

npm install --save-dev @types/three

创建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
    <script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.174.0/build/three.module.js",
            "three/addons/": "https://esm.sh/three@0.174.0/examples/jsm/"
        }
    }
    </script>
    <script type="module" src="./index.js"></script>
</body>
</html>

创建 index.js

import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();

scene.add(mesh);

const axesHelper = new THREE.AxesHelper(200);
scene.add(axesHelper);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
camera.position.set(0, 100, 200);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Camera、Renderer,启用 OrbitControls 轨道控制器。

EllipseCurve

然后写下 mesh.js

import * as THREE from 'three';

const arc = new THREE.EllipseCurve(0, 0, 100, 50);
const pointsList = arc.getPoints(20);

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsList);

const material = new THREE.PointsMaterial({
    color: new THREE.Color('orange'),
    size: 10
});

const points = new THREE.Points(geometry, material);

console.log(points);

export default points;

这里我们用 EllipseCurve 画一条椭圆曲线,椭圆中心是 0,0,长短半轴长分别是 100、50

用 getPoints 方法从中取出一些点的坐标,传入的是分段数,20 段就是 21 个点。

用这 21 个点的坐标设置为 BufferGeometry 的顶点,通过 setFromPoints 方法。

之后创建点模型。

看下效果:

npx live-server

image.png

可以看到,确实是一个椭圆的形状:

image.png

打开 devtools 看下:

image.png

可以看到,geometry.attributes.position 就是 21

也就是说曲线 API 就是一些计算曲线坐标的公式,从中取出一些点用点模型或者线模型画出来。

我们试下线模型:

image.png

const material = new THREE.LineBasicMaterial({
    color: new THREE.Color('orange')
});

const line = new THREE.Line(geometry, material);

image.png

这种就可以用来做行星轨道了。

当然,你要更光滑的话取更多的点就可以了。

圆是椭圆的一种特殊情况,把长短半轴长设置为一样就可以了:

image.png

image.png

所以圆弧也是这个 API。

你还可以指定画的角度:

image.png

比如 0 到 90 度:

image.png

SplineCurve

当然,椭圆、圆这种曲线都太规则了,如果我们想画一些不规则的曲线呢?

比如任意的一堆点连起来的曲线。

这就要用别的 API 了。

创建 mesh2.js

import * as THREE from 'three';

const arr = [
    new THREE.Vector2( -100, 0 ),
	new THREE.Vector2( -50, 50 ),
	new THREE.Vector2( 0, 0 ),
	new THREE.Vector2( 50, -50 ),
	new THREE.Vector2( 100, 0 )
];

const curve = new THREE.SplineCurve(arr);
const pointsArr = curve.getPoints(20);

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr);

const material = new THREE.LineBasicMaterial({ 
    color: new THREE.Color('orange') 
});

const line = new THREE.Line( geometry, material );

export default line;

如果我们已经有 5 个点,想让这 5 个点连成一条曲线,就用样条曲线 SplineCurve 的 api。

看下效果:

image.png

image.png

这样不明显,我们把点也画出来:

image.png

const pointsMaterial = new THREE.PointsMaterial({
    color: new THREE.Color('pink'),
    size: 5
});
const points = new THREE.Points(geometry, pointsMaterial);
line.add(points);

用这个 geometry 和点模型的材质创建点模型,加到 line 下面。

image.png

可以看到,SplineCurve 会画出穿过你给的那些点的曲线。

我们加个任意的点试试:

image.png

image.png

我们把传入的几个点单独标出来:

image.png

const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints(arr);
const material2 = new THREE.PointsMaterial({
    color: new THREE.Color('green'),
    size: 10
});
const points2 = new THREE.Points(geometry2, material2);
const line2 = new THREE.Line(geometry2, new THREE.LineBasicMaterial());
line.add(points2, line2);

image.png

可以看到,样条曲线 SplineCurve 确实是会把你传入的点用曲线连接起来。

QuadraticBezierCurve

如果你觉得这些曲线,曲率不是很大,想自己控制曲率呢?

这种就要用贝塞尔曲线了:

创建 mesh3.js

import * as THREE from 'three';

const p1 = new THREE.Vector2(0, 0);
const p2 = new THREE.Vector2(50, 100);
const p3 = new THREE.Vector2(100, 0);

const curve = new THREE.QuadraticBezierCurve(p1, p2, p3);
const pointsArr = curve.getPoints(20);

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr);

const material = new THREE.LineBasicMaterial({ 
    color: new THREE.Color('orange')
});

const line = new THREE.Line( geometry, material );

const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints([p1,p2,p3]);
const material2 = new THREE.PointsMaterial({
    color: new THREE.Color('pink'),
    size: 5
});
const points2 = new THREE.Points(geometry2, material2);
const line2 = new THREE.Line(geometry2, new THREE.LineBasicMaterial());
line.add(points2, line2);

export default line;

我们用 QuadraticBezierCurve 的 api 创建了贝塞尔曲线,传入 3 个点,第二个为控制点。

之后又把这三个点单独用点模型、线模型画了出来。

看下效果:

image.png

image.png

和样条曲线不同,贝塞尔曲线中间那个点是控制曲率的,把它改大一点试试:

image.png

image.png

现在明显曲率更大了。

CubicBezierCurve3

二次贝塞尔曲线是在一个平面上弯曲,如果是三次贝塞尔曲线,就是在三维空间内弯曲了。

创建 mesh4.js

import * as THREE from 'three';

const p1 = new THREE.Vector3(-100, 0, 0);
const p2 = new THREE.Vector3(50, 100, 0);
const p3 = new THREE.Vector3(100, 0, 100);
const p4 = new THREE.Vector3(100, 0, 0);

const curve = new THREE.CubicBezierCurve3(p1, p2, p3, p4);
const pointsArr = curve.getPoints(20);

const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr);

const material = new THREE.LineBasicMaterial({ 
    color: new THREE.Color('orange')
});

const line = new THREE.Line( geometry, material );

const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints([p1,p2,p3,p4]);
const material2 = new THREE.PointsMaterial({
    color: new THREE.Color('pink'),
    size: 5
});
const points2 = new THREE.Points(geometry2, material2);
const line2 = new THREE.Line(geometry2, new THREE.LineBasicMaterial());
line.add(points2, line2);

export default line;

用 CubicBezierCurve3 这个 API,传入 4 个点,中间两个是控制点。

其余部分一样。

跑下看看:

image.png

2025-03-22 13.07.45.gif

可以看到,是一条三维曲线,第一个和第四个点是端点,中间两个是控制点。

CurvePath

有的时候,一条曲线可能是由多条曲线复合而成的,如果你想组合多条曲线,就可以用 CurvePath 的 api。

创建 mesh5.js

import * as THREE from 'three';

const p1 = new THREE.Vector2(0, 0);
const p2 = new THREE.Vector2(100, 100);
const line1 = new THREE.LineCurve(p1, p2);

const arc = new THREE.EllipseCurve(0, 100, 100 , 100, 0, Math.PI);

const p3 = new THREE.Vector2(-100, 100);
const p4 = new THREE.Vector2(0, 0);
const line2 = new THREE.LineCurve(p3, p4);

const curvePath = new THREE.CurvePath();
curvePath.add(line1);
curvePath.add(arc);
curvePath.add(line2);

const pointsArr = curvePath.getPoints(20);
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr);

const material = new THREE.LineBasicMaterial({
    color: new THREE.Color('pink')
});

const line = new THREE.Line(geometry, material);

export default line;

圆是椭圆的特殊情况,直线也是曲线的特殊情况,所以也归为曲线。

我们创建了两条直线 LineCure、一个椭圆曲线 EllipseCurve

用曲线路径 CurvePath 把它们组合起来。

看下效果:

image.png

image.png

这样,就可以组合出复杂的曲线形状了。

案例代码上传了小册仓库

总结

很多时候都要画曲线,比如行星的轨道、地图的飞线。

这节我们学了下曲线的 API。

很多 3D 场景中,需要画一些曲线,到时候就会用到这些曲线 API。

评论