Skip to content

13. 实战:云雷纹

Published:

记得小学的时候考完试,离交卷还早,我就会在草稿纸上乱花一些花纹。

一圈一圈的,比如这样:

image.png

image.png

而在 3D 场景中,这种花纹也有很重要的装饰作用,比如一些大屏的背景装饰:

image.png

学完画曲线的 API 之后,我们就可以来画这些花纹了。

创建 vite 项目:

npx create-vite cloudscape-figure

image.png

选择创建不用框架的 js 项目。

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three

改下 src/main.js

import './style.css';
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 light = new THREE.DirectionalLight(0xffffff);
light.position.set(500, 300, 600);
scene.add(light);

const light2 = new THREE.AmbientLight();
scene.add(light2);

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

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

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

const renderer = new THREE.WebGLRenderer({
  antialias: true
});
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、Light、Camera、Renderer

改下 style.css

body {
    margin: 0;
}

然后创建 mesh.js

import * as THREE from 'three';

const arc = new THREE.EllipseCurve(0, 0, 300 , 300, 0, Math.PI * 2);

const pointsArr = arc.getPoints(50);
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;

这里用 BufferGeometry + Line 画了一条圆的曲线。

跑一下:

npm run dev

image.png

2025-08-09 21.55.14.gif

然后看下这个云雷纹:

image.png

从里往外画,先是两个圆环:

import * as THREE from 'three';

const group = new THREE.Group();

const arc1 = new THREE.EllipseCurve(0, 0, 100 , 100, 0, Math.PI * 2);
const pointsArr1 = arc1.getPoints(50);
const geometry1 = new THREE.BufferGeometry();
geometry1.setFromPoints(pointsArr1);
const material1 = new THREE.LineBasicMaterial({
    color: new THREE.Color('white'),
    lineWidth: 5
});
const line1 = new THREE.Line(geometry1, material1);
group.add(line1);

const arc2 = new THREE.EllipseCurve(0, 0, 120 , 120, 0, Math.PI * 2);
const pointsArr2 = arc2.getPoints(50);
const geometry2 = new THREE.BufferGeometry();
geometry2.setFromPoints(pointsArr2);
const material2 = new THREE.LineBasicMaterial({
    color: new THREE.Color('white')
});
const line2 = new THREE.Line(geometry2, material2);
group.add(line2);

export default group;

同样的方式画两个圆,半径不一样。

image.png

内圆线粗一些,但你会发现设置 lineWidth 不生效。

这是一个常见的问题。

需要换 API,换成这三个:

image.png

const geometry1 = new LineGeometry();
geometry1.setFromPoints(pointsArr1);
const material1 = new LineMaterial({
    color: new THREE.Color('white'),
    linewidth: 5
});
const line1 = new Line2(geometry1, material1);
group.add(line1);

用 LineGeometry、LineMaterial、Line2 代替原来的 BufferGeometry、LineBasicMaterial、Line 的 API

再看下:

image.png

现在 lineWidth 就生效了。

继续来画:

image.png

再往外是一些圆。

我们再创建一个 group,里面单独画一些圆:

image.png

之前圆半径是 120,现在每个小圆半径 10,那就是从 130 的半径开始画。

角度从 0 到 2π,每个角度的位置用 R * Math.cos(angle) 和 R * Math.sin(angle) 确定圆心位置。

const circleGroup = new THREE.Group();
for(let angle = 0; angle < Math.PI  * 2; angle += Math.PI  / 12) {
    const R = 130;
    const x = R * Math.cos(angle);
    const y = R * Math.sin(angle);
    const arc = new THREE.EllipseCurve(x, y, 10, 10, 0, Math.PI * 2);
    const pointsArr = arc.getPoints(50);
    const geometry = new THREE.BufferGeometry();
    geometry.setFromPoints(pointsArr);
    const material = new THREE.LineBasicMaterial({
        color: new THREE.Color('white')
    });
    const line = new THREE.Line(geometry, material);
    circleGroup.add(line);
}
group.add(circleGroup);

看下效果:

image.png

然后再画这一圈粗的:

image.png

image.png

因为线宽 5,那半径就要额外加 2.5,不然线会和圆重叠

const arc3 = new THREE.EllipseCurve(0, 0, 142.5 , 142.5, 0, Math.PI * 2);
const pointsArr3 = arc3.getPoints(50);
const geometry3 = new LineGeometry();
geometry3.setFromPoints(pointsArr3);
const material3 = new LineMaterial({
    color: new THREE.Color('white'),
    linewidth: 5
});
const line3 = new Line2(geometry3, material3);
group.add(line3);

image.png

对比下:

image.png

内圈圆间距太大了,改下半径:

image.png

image.png

然后继续画外面的:

image.png

这个和那一圈圆差不多,也是用 group 添加多个单独的花纹。

每个花纹独立画

具体的花纹可以用 BufferGeometry + Line 来画,计算 11 个点的坐标就行。

image.png

点不多,不用找规律,直接枚举就行:

image.png

const figureGroup = new THREE.Group();
for(let angle = 0; angle < Math.PI  * 2; angle += Math.PI  / 10) {
    const R = 200;
    const x = R * Math.cos(angle);
    const y = R * Math.sin(angle);
    
    const pointsArr = [
        new THREE.Vector3(0, 0, 0),
        new THREE.Vector3(10, 0, 0),
        new THREE.Vector3(10, 10, 0),
        new THREE.Vector3(-10, 10, 0),
        new THREE.Vector3(-10, -10, 0),
        new THREE.Vector3(20, -10, 0),
        new THREE.Vector3(20, 20, 0),
        new THREE.Vector3(-20, 20, 0),
        new THREE.Vector3(-20, -20, 0),
        new THREE.Vector3(20, -20, 0)
    ];

    const geometry = new THREE.BufferGeometry();
    geometry.setFromPoints(pointsArr);
    const material = new THREE.LineBasicMaterial({
        color: new THREE.Color('white')
    });
    const line = new THREE.Line(geometry, material);
    
    line.position.set(x, y, 0);    
    line.rotation.z = angle;
    
    figureGroup.add(line);
}
group.add(figureGroup);

在原点把 11 个点画出来,然后设置这个形状的 position 和 scale

看下效果:

image.png 对比下:

image.png

没啥问题,然后再画最后一圈就好了:

image.png

这一圈花纹大小大一些

const figureGroup2 = new THREE.Group();
for(let angle = 0; angle < Math.PI  * 2; angle += Math.PI  / 10) {
    const R = 260;
    const x = R * Math.cos(angle);
    const y = R * Math.sin(angle);

    const pointsArr = [
        new THREE.Vector3(0, 0, 0),
        new THREE.Vector3(15, 0, 0),
        new THREE.Vector3(15, 15, 0),
        new THREE.Vector3(-15, 15, 0),
        new THREE.Vector3(-15, -15, 0),
        new THREE.Vector3(30, -15, 0),
        new THREE.Vector3(30, 30, 0),
        new THREE.Vector3(-30, 30, 0),
        new THREE.Vector3(-30, -30, 0),
        new THREE.Vector3(30, -30, 0)
    ];

    const geometry = new THREE.BufferGeometry();
    geometry.setFromPoints(pointsArr);
    const material = new THREE.LineBasicMaterial({
        color: new THREE.Color('white')
    });
    const line = new THREE.Line(geometry, material);
    
    line.position.set(x, y, 0);    
    line.rotation.z = angle;
    
    figureGroup2.add(line);
}
group.add(figureGroup2);

看下画好的花纹:

image.png

我们可以让它转起来,每一圈转速不同,一正一反的转:

image.png

mesh.children.forEach((item, index) => {
    const flag = index % 2 === 0 ? 1 : -1;
    item.rotation.z += 0.001 * index * flag;
})

看下整体效果:

2025-08-09 23.35.36.gif

等后面学完模型后,用它来作为佛打坐的底盘就很不错:

2025-08-09 23.11.49.gif

案例代码上传了小册仓库

总结

这节我们画了一个云雷纹,练习了下曲线 Curve 的 API,并且学了 lineWidth 不生效的解决方案。

lineWidth 不生效需要换成 LineGeometry、LineMaterial、Line2 的 API

然后我们一圈圈的画了曲线图案,主要是 BufferGeometry 自定义顶点 + Line 来画。

也有的是 Curve 然后取点设置到 BufferGeometry

总之,只要确定了一些点,用线模型连接画出来就行。

评论