Skip to content
章节导航

threejs 几何体

内置几何体

three.js 的内置几何体大致可分成以下几类:

  • 二维几何体
    • PlaneGeometry 矩形平面
    • CircleGeometry 圆形平面
    • RingGeometry 圆环平面
    • ShapeGeometry 二维图形
  • 三维几何体
    • BoxGeometry 立方体
    • TetrahedronGeometry 四面体
    • OctahedronGeometry 八面体
    • DodecahedronGeometry 十二面体
    • IcosahedronGeometry 二十面体
    • PolyhedronGeometry 多面体
    • SphereGeometry 球体
    • ConeGeometry 圆锥
    • CylinderGeometry 圆柱
    • TorusGeometry 三维圆环
    • TorusKnotGeometry 扭结
  • 路径合成几何体
    • TubeGeometry 管道
    • LatheGeometry 车削
    • ExtrudeGeometry 挤压
  • 线性几何体
    • WireframeGeometry 网格几何体
    • EdgesGeometry 边缘几何体

接下来,咱们详细说一下其绘制方法。

1-1 - 二维几何体

PlaneGeometry 矩形平面

image-20220504000008737

PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)

  • width — 平面沿着 X 轴的宽度。默认值是 1。
  • height — 平面沿着 Y 轴的高度。默认值是 1。
  • widthSegments — (可选)平面的宽度分段数,默认值是 1。
  • heightSegments — (可选)平面的高度分段数,默认值是 1。

代码实例:

  1. 先参照上一篇的项目架构一个一个 react+ts 项目,详情参见源码。
  2. 建立一个 Stage 对象,把渲染器、场景、相机、轨道控制器和响应式布局都封装进去。这样在写例子的时候会比较方便、整洁。
  • src/component/Stage.ts
import { PerspectiveCamera, Scene, WebGLRenderer } from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

export default class Stage {
  
  renderer: WebGLRenderer;
  
  scene: Scene;
  
  camera: PerspectiveCamera;
  
  controls: OrbitControls;
  
  beforeRender = (time: number = 0) => {};

  
  constructor(x: number = 0, y: number = 0, z: number = 12) {
    this.scene = new Scene();
    this.renderer = new WebGLRenderer({ antialias: true });
    const { clientWidth, clientHeight } = this.renderer.domElement;
    this.renderer.setSize(clientWidth * devicePixelRatio, clientHeight * devicePixelRatio, false);
    this.camera = new PerspectiveCamera(45, clientWidth / clientHeight, 0.1, 1000);
    this.camera.position.set(x, y, z);
    this.camera.lookAt(0, 0, 0);
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
  }

  
  responsive() {
    const { renderer, camera } = this;
    if (this.resizeRendererToDisplaySize(renderer)) {
      const { clientWidth, clientHeight } = renderer.domElement;
      camera.aspect = clientWidth / clientHeight;
      camera.updateProjectionMatrix();
    }
  }

  
  resizeRendererToDisplaySize(renderer: WebGLRenderer): boolean {
    const { width, height, clientWidth, clientHeight } = renderer.domElement;
    const [w, h] = [clientWidth * devicePixelRatio, clientHeight * devicePixelRatio];
    const needResize = width !== w || height !== h;
    if (needResize) {
      renderer.setSize(w, h, false);
    }
    return needResize;
  }

  
  animate(time = 0) {
    this.responsive();
    this.beforeRender(time);
    this.renderer.render(this.scene, this.camera);
    requestAnimationFrame((time) => {
      this.animate(time);
    });
  }
}
  1. 绘制矩形平面。
  • src/component/Plane.ts
import React, { useRef, useEffect } from "react";
import {
  Mesh,
  MeshBasicMaterial,
  MeshNormalMaterial,
  PlaneGeometry,
} from "three";
import Stage from "../component/Stage";
import "./fullScreen.css";

const stage = new Stage();
const { scene, renderer } = stage;

const geometry = new PlaneGeometry();

{
  const material = new MeshNormalMaterial({
    polygonOffset: true,
    polygonOffsetFactor: 1,
    polygonOffsetUnits: 1,
  });
  const mesh = new Mesh(geometry, material);
  scene.add(mesh);
}
{
  const material = new MeshBasicMaterial({
    wireframe: true,
  });
  const mesh = new Mesh(geometry, material);
  scene.add(mesh);
}

const Plane: React.FC = (): JSX.Element => {
  const divRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const { current } = divRef;
    if (current) {
      current.innerHTML = "";
      current.append(renderer.domElement);
      stage.animate();
    }
  }, []);
  return <div ref={divRef} class></div>;
};

export default Plane;

CircleGeometry 圆形平面

image-20220504000121351

CircleGeometry(radius : Float, segments : Integer, thetaStart : Float, thetaLength : Float)

  • radius — 圆形的半径,默认值为 1
  • segments — 分段(三角面)的数量,最小值为 3,默认值为 8。
  • thetaStart — 第一个分段的起始角度,默认为 0。(three o'clock position)
  • thetaLength — 圆形扇区的中心角,通常被称为 “θ”(西塔)。默认值是 2*Pi,这使其成为一个完整的圆。

RingGeometry 圆环平面

image-20220504000228776

RingGeometry(innerRadius : Float, outerRadius : Float, thetaSegments : Integer, phiSegments : Integer, thetaStart : Float, thetaLength : Float)

  • innerRadius — 内部半径,默认值为 0.5。
  • outerRadius — 外部半径,默认值为 1。
  • thetaSegments — 圆环的分段数。这个值越大,圆环就越圆。最小值为 3,默认值为 8。
  • phiSegments — 最小值为 1,默认值为 8。
  • thetaStart — 起始角度,默认值为 0。
  • thetaLength — 圆心角,默认值为 Math.PI * 2。

ShapeGeometry 二维图形

ShapeGeometry(shapes : Array, curveSegments : Integer)

  • shapes — 一个单独的 shape,或者一个包含形状的 Array。
  • curveSegments - Integer - 每一个形状的分段数,默认值为 12。

代码示例:

const shape = new Shape();
shape.moveTo(0, 0);
shape.bezierCurveTo(1, 1, -1, 1, 0, 0);
const geometry = new ShapeGeometry(shape);

效果如下:

image-20220503232630818

1-2 - 三维几何体

BoxGeometry 立方体

image-20220504085446202

BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)

  • width — X 轴上面的宽度,默认值为 1。
  • height — Y 轴上面的高度,默认值为 1。
  • depth — Z 轴上面的深度,默认值为 1。
  • widthSegments — (可选)宽度的分段数,默认值是 1。
  • heightSegments — (可选)高度的分段数,默认值是 1。
  • depthSegments — (可选)深度的分段数,默认值是 1。

TetrahedronGeometry 四面体

image-20220504085814399

TetrahedronGeometry(radius : Float, detail : Integer)

  • radius — 四面体的半径,默认值为 1。
  • detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个四面体。

OctahedronGeometry 八面体

image-20220504095541337

OctahedronGeometry(radius : Float, detail : Integer)

  • radius — 八面体的半径,默认值为 1。
  • detail — 默认值为 0,将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个八面体。

DodecahedronGeometry 十二面体

image-20220504095710956

DodecahedronGeometry(radius : Float, detail : Integer)

  • radius — 十二面体的半径,默认值为 1。
  • detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个十二面体。

IcosahedronGeometry 二十面体

image-20220504095829094

IcosahedronGeometry(radius : Float, detail : Integer)

  • radius — 二十面体的半径,默认为 1。
  • detail — 默认值为 0。将这个值设为一个大于 0 的数将会为它增加一些顶点,使其不再是一个二十面体。当这个值大于 1 的时候,实际上它将变成一个球体。

PolyhedronGeometry 多面体

PolyhedronGeometry(vertices : Array, indices : Array, radius : Float, detail : Integer

  • vertices — 一个顶点 Array(数组):[1,1,1, -1,-1,-1, ...]。
  • indices — 一个构成面的索引 Array(数组), [0,1,2, 2,3,0, ...]。
  • radius — Float - 最终形状的半径。
  • detail — Integer - 将对这个几何体细分多少个级别。细节越多,形状就越平滑。

代码示例:

const geometry = new PolyhedronGeometry(
  [-1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1],
  [2, 1, 0, 0, 3, 2, 0, 4, 7, 7, 3, 0, 0, 1, 5, 5, 4, 0, 1, 2, 6, 6, 5, 1, 2, 3, 7, 7, 6, 2, 4, 5, 6, 6, 7, 4],
  1,
  1
);

效果如下:

image-20220504110801396

SphereGeometry 球体

image-20220504110937583

SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)

  • radius — 球体半径,默认为 1。
  • widthSegments — 水平分段数(沿着经线分段),最小值为 3,默认值为 32。
  • heightSegments — 垂直分段数(沿着纬线分段),最小值为 2,默认值为 16。
  • phiStart — 指定水平(经线)起始角度,默认值为 0。。
  • phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
  • thetaStart — 指定垂直(纬线)起始角度,默认值为 0。
  • thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。

该几何体是通过扫描并计算围绕着 Y 轴(水平扫描)和 X 轴(垂直扫描)的顶点来创建的。

因此,我们可以通过为 phiStart,phiLength,thetaStart 和 thetaLength 属性对球体进行切片。

ConeGeometry 圆锥

image-20220504111033734

ConeGeometry(radius : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)

  • radius — 圆锥底部的半径,默认值为 1。
  • height — 圆锥的高度,默认值为 1。
  • radialSegments — 圆锥侧面周围的分段数,默认为 8。
  • heightSegments — 圆锥侧面沿着其高度的分段数,默认值为 1。
  • openEnded — 一个 Boolean 值,指明该圆锥的底面是开放的还是封顶的。默认值为 false,即其底面默认是封顶的。
  • thetaStart — 第一个分段的起始角度,默认为 0。(three o'clock position)
  • thetaLength — 圆锥底面圆扇区的中心角,通常被称为 “θ”(西塔)。默认值是 2*Pi,这使其成为一个完整的圆锥。

CylinderGeometry 圆柱

image-20220504111139329

CylinderGeometry(radiusTop : Float, radiusBottom : Float, height : Float, radialSegments : Integer, heightSegments : Integer, openEnded : Boolean, thetaStart : Float, thetaLength : Float)

  • radiusTop — 圆柱的顶部半径,默认值是 1。
  • radiusBottom — 圆柱的底部半径,默认值是 1。
  • height — 圆柱的高度,默认值是 1。
  • radialSegments — 圆柱侧面周围的分段数,默认为 8。
  • heightSegments — 圆柱侧面沿着其高度的分段数,默认值为 1。
  • openEnded — 一个 Boolean 值,表示该圆柱的底面和顶面是否开放。默认值为 false,即闭合。
  • thetaStart — 第一个分段的起始角度,默认为 0。(three o'clock position)
  • thetaLength — 圆柱底面圆扇区的中心角,通常被称为 “θ”(西塔)。默认值是 2*Pi,这使其成为一个完整的圆柱。

TorusGeometry 三维圆环

image-20220504111253084

TorusGeometry(radius : Float, tube : Float, radialSegments : Integer, tubularSegments : Integer, arc : Float)

  • radius - 环面的半径,从环面的中心到管道横截面的中心。默认值是 1。
  • tube — 管道的半径,默认值为 0.4。
  • radialSegments — 管道横截面的分段数,默认值为 8。
  • tubularSegments —圆环 x 的分段数,默认值为 6。
  • arc — 圆环的圆心角(单位是弧度),默认值为 Math.PI * 2。

TorusKnotGeometry 扭结

image-20220504111338713

TorusKnotGeometry(radius : Float, tube : Float, tubularSegments : Integer, radialSegments : Integer, p : Integer, q : Integer)

radius - 圆环的半径,默认值为 1。

tube — 管道的半径,默认值为 0.4。

tubularSegments — 扭结线的分段数量,默认值为 64。

radialSegments — 管道分段数量,默认值为 8。

p — 这个值决定了几何体将绕着其旋转对称轴旋转多少次,默认值是 2。

q — 这个值决定了几何体将绕着其内部圆环旋转多少次,默认值是 3。

1-3 - 路径合成几何体

TubeGeometry 管道

image-20220504135429956

TubeGeometry(path : Curve, tubularSegments : Integer, radius : Float, radialSegments : Integer, closed : Boolean)

  • path — Curve - 一个由基类 Curve 继承而来的 3D 路径。 Default is a quadratic bezier curve.
  • tubularSegments — Integer - 组成这一管道的分段数,默认值为 64。
  • radius — Float - 管道的半径,默认值为 1。
  • radialSegments — Integer - 管道横截面的分段数目,默认值为 8。
  • closed — Boolean 管道的两端是否闭合,默认值为 false。

代码示例:

  1. 自定义一个绘制曲线的 curve 对象
  • src/component/CustomSinCurve
import { Curve, Vector3 } from "three";

export class CustomSinCurve extends Curve<Vector3> {
  getPoint(t: number): Vector3 {
    const a = t * 4 + 1;
    const ty = (a * Math.cos(t * Math.PI * 2 * 4)) / 8;
    const tz = (a * Math.sin(t * Math.PI * 2 * 4)) / 8;
    return new Vector3(t, ty, tz);
  }
}
  1. 建立管道几何体
const path = new CustomSinCurve();
const geometry = new TubeGeometry(path, 128, 0.05, 3, false);

LatheGeometry 车削

image-20220504142139777

LatheGeometry(points : Array, segments : Integer, phiStart : Float, phiLength : Float)

  • points — 一个 Vector2 对象数组。每个点的 X 坐标必须大于 0。
  • segments — 要生成的车削几何体圆周分段的数量,默认值是 12。
  • phiStart — 以弧度表示的起始角度,默认值为 0。
  • phiLength — 车削部分的弧度(0-2PI)范围,2PI 将是一个完全闭合的、完整的车削几何体,小于 2PI 是部分的车削。默认值是 2PI。

代码示例:

const points = [];
for (let i = 0; i < 1; i += 0.1) {
  const x = (Math.sin(i * Math.PI * 1.8 + 2.8) + 1.1) / 5;
  points.push(new Vector2(x, i));
}
const geometry = new LatheGeometry(points, 32, 0, Math.PI);

ExtrudeGeometry 挤压

image-20220504191609116

ExtrudeGeometry(shapes : Array, options : Object)

  • shapes — 形状或者一个包含形状的数组。
  • options — 一个包含有下列参数的对象:
    • steps — int,用于沿着挤出样条的深度细分的点的数量,默认值为 1。
    • depth — float,挤出的形状的深度,默认值为 1。
    • bevelEnabled — bool,对挤出的形状应用是否斜角,默认值为 true。
    • bevelThickness — float,设置原始形状上斜角的厚度。默认值为 0.2。
    • bevelSize — float。斜角与原始形状轮廓之间的延伸距离,默认值为 bevelThickness-0.1。
    • bevelOffset — float. Distance from the shape outline that the bevel starts. Default is 0.
    • bevelSegments — int。斜角的分段层数,默认值为 3。
    • extrudePath — THREE.Curve 对象。一条沿着被挤出形状的三维样条线。Bevels not supported for path extrusion.
    • UVGenerator — Object。提供了 UV 生成器函数的对象。

该对象可以将一个二维形状挤成一个三维几何体。

当使用这个几何体创建 Mesh 的时候,如果你希望分别对它的表面和它挤出的侧面使用单独的材质,你可以使用一个材质数组。 第一个材质将用于其表面;第二个材质则将用于其挤压出的侧面。

代码示例:

const shape = new Shape();
shape.moveTo(0, 0);
shape.lineTo(0, 1);
shape.lineTo(1, 1);
shape.lineTo(1, 0);
shape.lineTo(0, 0);

const extrudeSettings = {
  steps: 1,
  depth: 1,
  bevelEnabled: true,
  bevelThickness: 0.2,
  bevelSize: 0.1,
  bevelOffset: 0,
  bevelSegments: 1,
};

const geometry = new ExtrudeGeometry(shape, extrudeSettings);

除此之外,我们还可以通过 extrudePath 属性让一个二维图形沿 curve 路径做挤压。

const shape = new Shape();
shape.moveTo(0, 0);
shape.lineTo(0.1, 0);
shape.lineTo(0.1, 0.1);
const path = new CustomSinCurve();
const geometry = new ExtrudeGeometry(shape, {
  steps: 128,
  extrudePath: path,
});

效果如下:

image-20220510234312619

1-4 - 线性几何体

WireframeGeometry 网格几何体

image-20220504210416920

WireframeGeometry(geometry : BufferGeometry)

  • geometry — 任意几何体对象。

代码示例:

const line = new LineSegments(wireframe);
const mat = line.material as LineBasicMaterial;
mat.depthTest = false;
mat.opacity = 0.5;
mat.transparent = true;

EdgesGeometry 边缘几何体

image-20220504212043725

EdgesGeometry(geometry : BufferGeometry, thresholdAngle : Integer)

  • geometry — 任何一个几何体对象。
  • thresholdAngle — 仅当相邻面的法线之间的角度(单位为角度)超过这个值时,才会渲染边缘。默认值为 1。

代码示例:

const geometry = new SphereGeometry(2, 8, 8);
const edges = new EdgesGeometry(geometry);
const line = new LineSegments(edges);

自定义几何体

1 - 基本概念

BufferGeometry 是所有 three.js 内置几何体的基类,通过此基类可以自定义几何体。

BufferGeometry 具备以下重要属性:

  • position 顶点位置
  • index 顶点索引
  • normal 法线
  • uv 坐标
  • color 顶点颜色

image.png

从上图可以看出,在顶点索引为 4 的地方,position 、normal 、uv 、color 是基于顶点一一对应的。

接下来咱们自己自定义一个几何体。

2 - 自定义立方体

  1. 在下面的 vertices 里,我们按照对立三角形的绘图方式,定义了立方体的 6 个面,共 36 个顶点。
const vertices = [
  
  { pos: [-1, -1,  1], norm: [ 0,  0,  1], uv: [0, 0], },
  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 0], },
  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 1], },
 
  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 1], },
  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 0], },
  { pos: [ 1,  1,  1], norm: [ 0,  0,  1], uv: [1, 1], },
  
  { pos: [ 1, -1,  1], norm: [ 1,  0,  0], uv: [0, 0], },
  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 0], },
  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 1], },
 
  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 1], },
  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 0], },
  { pos: [ 1,  1, -1], norm: [ 1,  0,  0], uv: [1, 1], },
  
  { pos: [ 1, -1, -1], norm: [ 0,  0, -1], uv: [0, 0], },
  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 0], },
  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 1], },
 
  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 1], },
  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 0], },
  { pos: [-1,  1, -1], norm: [ 0,  0, -1], uv: [1, 1], },
  
  { pos: [-1, -1, -1], norm: [-1,  0,  0], uv: [0, 0], },
  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 0], },
  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 1], },
 
  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 1], },
  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 0], },
  { pos: [-1,  1,  1], norm: [-1,  0,  0], uv: [1, 1], },
  
  { pos: [ 1,  1, -1], norm: [ 0,  1,  0], uv: [0, 0], },
  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 0], },
  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 1], },
 
  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 1], },
  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 0], },
  { pos: [-1,  1,  1], norm: [ 0,  1,  0], uv: [1, 1], },
  
  { pos: [ 1, -1,  1], norm: [ 0, -1,  0], uv: [0, 0], },
  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 0], },
  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 1], },
 
  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 1], },
  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 0], },
  { pos: [-1, -1, -1], norm: [ 0, -1,  0], uv: [1, 1], },
];
  1. 按照属性将这些顶点分成三组:
const positions = [];
const normals = [];
const uvs = [];
for (const vertex of vertices) {
  positions.push(...vertex.pos);
  normals.push(...vertex.norm);
  uvs.push(...vertex.uv);
}
  1. 基于 positions、normals 和 uvs 建立 BufferAttribute 对象。
const geometry = new BufferGeometry();
const positionNumComponents = 3;
const normalNumComponents = 3;
const uvNumComponents = 2;
const positionAttr=new BufferAttribute(
  new Float32Array(positions), 
  positionNumComponents
)
const normalAttr=new BufferAttribute(
  new Float32Array(normals), 
  normalNumComponents
)
const uvAttr=new BufferAttribute(
  new Float32Array(uvs), 
  uvNumComponents
)

BufferAttribute 对象就是对顶点着色器中 Attribute 变量的管理,通过此对象可以存储顶点点位、顶点数量、矢量长度等,并可以对其进行矩阵变换、拷贝、读写等。

  1. 将 BufferAttribute 对象添加到 geometry 几何体中。
geometry.setAttribute('position',positionAttr);
geometry.setAttribute('normal',normalAttr);
geometry.setAttribute('uv',uvAttr);

在 setAttribute() 中,'position'、'normal'、'uv'、'color' 都是内置 attribute 变量名,不能随便写。

当然如果我们想自定义 attribute 变量名,那就可以随便写了,只要符合基本的命名规范就行。

几何体形状如下:

image-20220505114047411

自定义几何体的基本方法就是这样,接下来我们还可以使用顶点索引自定义几何体。

3 - 顶点索引

  1. 将 vertices 设置为 4*6=24 个点,一个面 4 个点。
const vertices = [
  
  { pos: [-1, -1,  1], norm: [ 0,  0,  1], uv: [0, 0], }, 
  { pos: [ 1, -1,  1], norm: [ 0,  0,  1], uv: [1, 0], }, 
  { pos: [-1,  1,  1], norm: [ 0,  0,  1], uv: [0, 1], }, 
  { pos: [ 1,  1,  1], norm: [ 0,  0,  1], uv: [1, 1], }, 
  
  { pos: [ 1, -1,  1], norm: [ 1,  0,  0], uv: [0, 0], }, 
  { pos: [ 1, -1, -1], norm: [ 1,  0,  0], uv: [1, 0], }, 
  { pos: [ 1,  1,  1], norm: [ 1,  0,  0], uv: [0, 1], }, 
  { pos: [ 1,  1, -1], norm: [ 1,  0,  0], uv: [1, 1], }, 
  
  { pos: [ 1, -1, -1], norm: [ 0,  0, -1], uv: [0, 0], }, 
  { pos: [-1, -1, -1], norm: [ 0,  0, -1], uv: [1, 0], }, 
  { pos: [ 1,  1, -1], norm: [ 0,  0, -1], uv: [0, 1], }, 
  { pos: [-1,  1, -1], norm: [ 0,  0, -1], uv: [1, 1], }, 
  
  { pos: [-1, -1, -1], norm: [-1,  0,  0], uv: [0, 0], }, 
  { pos: [-1, -1,  1], norm: [-1,  0,  0], uv: [1, 0], }, 
  { pos: [-1,  1, -1], norm: [-1,  0,  0], uv: [0, 1], }, 
  { pos: [-1,  1,  1], norm: [-1,  0,  0], uv: [1, 1], }, 
  
  { pos: [ 1,  1, -1], norm: [ 0,  1,  0], uv: [0, 0], }, 
  { pos: [-1,  1, -1], norm: [ 0,  1,  0], uv: [1, 0], }, 
  { pos: [ 1,  1,  1], norm: [ 0,  1,  0], uv: [0, 1], }, 
  { pos: [-1,  1,  1], norm: [ 0,  1,  0], uv: [1, 1], }, 
  
  { pos: [ 1, -1,  1], norm: [ 0, -1,  0], uv: [0, 0], }, 
  { pos: [-1, -1,  1], norm: [ 0, -1,  0], uv: [1, 0], }, 
  { pos: [ 1, -1, -1], norm: [ 0, -1,  0], uv: [0, 1], }, 
  { pos: [-1, -1, -1], norm: [ 0, -1,  0], uv: [1, 1], }, 
];
  1. 用 BufferGeometry.setIndex() 方法设置顶点索引。
geometry.setIndex([
  
   0,  1,  2,   2,  1,  3, 
  
   4,  5,  6,   6,  5,  7,  
  
   8,  9, 10,  10,  9, 11, 
  
  12, 13, 14,  14, 13, 15,  
  
  16, 17, 18,  18, 17, 19, 
  
  20, 21, 22,  22, 21, 23,  
]);

4 - 计算法线

BufferGeometry 有一个自己根据现有顶点计算法线的方法 computeVertexNormals()。

computeVertexNormals() 方法是按照逐顶点着色的方式计算法线的。

逐顶点着色的原理我们在 webgl 的光里说过。

代码示例:

geometry.setAttribute("position", positionAttr);

geometry.setAttribute("uv", uvAttr);
geometry.setIndex([0, 1, 2, 2, 1, 3, 4, 5, 6, 6, 5, 7, 8, 9, 10, 10, 9, 11, 12, 13, 14, 14, 13, 15, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23]);
geometry.computeVertexNormals();

我们可以用 VertexNormalsHelper 对象将一个 Mesh 对象的法线显示出来:

const cube = new Mesh(geometry, material);
const helper = new VertexNormalsHelper(cube);
scene.add(mesh, helper);

效果如下:

image-20220505154649428

在用 computeVertexNormals() 方法计算法线的时候,若在光滑的物体表面发现了明显的接缝线,那这可能就是由两排位置相同、法线不同的顶点引起的。

这需要我们自己根据模型特征调整法线,将这两排顶点的法线调成一样。

image-20220527223829778

5 - 顶点数据的更新

当我们想要修改几何体的顶点数据的时候,可以直接通过 BufferGeometry 对象下的 BufferAttribute 修改。

举个例子:我要在一秒后,把上面自定义的几何体打开一个口子。

image-20220507102640729

代码如下:

setTimeout(() => {
  positionAttr.setXYZ(18, 1, 2, 1);
  positionAttr.needsUpdate = true;
}, 1000);

接下来在渲染器执行 render() 方法时,便会将最新的顶点数据传递给顶点着色器里的 attribute 变量。

关于几何体的基础知识,咱们就说到这,接下来咱们做点好玩的东西,巩固咱们之前的所学。

案例 - 自定义波浪球

下面这个绚丽的波浪球就是我们接下来要做的,我之后还会让它蠕动来。其中会制涉及的知识点有自定义几何体的封装、几何体顶点数据的读写、正弦函数动画等。

image-20220507130846972

  1. 封装一个波浪球对象。
import {
    BufferGeometry,
    Vector3,
    Object3D,
    BufferAttribute,
    InterleavedBufferAttribute,
    Euler,
} from "three"
const pi2 = Math.PI * 2

export default class WaveBall extends BufferGeometry {
  
    widthSegments: number
    heightSegments: number
  
    a: number = 0.7
    omega: number = 12

    constructor(widthSegments: number = 18, heightSegments: number = 12) {
        super()
        this.widthSegments = widthSegments
        this.heightSegments = heightSegments
        this.init()
    }
    init() {
        const { widthSegments, heightSegments } = this
      
        const [width, height] = [widthSegments + 1, heightSegments + 1]
      
        const count = width * height
      
        const positions = new Float32Array(count * 3)
      
        const indices = []

      
        const setPos = SetPos(positions)

      
        for (let y = 0; y < height; ++y) {
          
            const lat = (y / heightSegments - 0.5) * Math.PI
            for (let x = 0; x < width; ++x) {
              
                const long = (x / widthSegments - 0.5) * Math.PI * 2
              
                setPos(lat, long)
              
                if (y && x) {
                  
                    const lt = (y - 1) * width + (x - 1)
                    const rt = (y - 1) * width + x
                    const lb = y * width + (x - 1)
                    const rb = y * width + x
                    indices.push(lb, rb, lt, lt, rb, rt)
                }
            }
        }
        this.setAttribute("position", new BufferAttribute(positions, 3))
        this.setIndex(indices)
        this.computeVertexNormals()
    }
  
    wave(phi = 0) {
        const { widthSegments, heightSegments, a, omega } = this
      
        const [width, height] = [widthSegments + 1, heightSegments + 1]
      
        const posAttr = this.getAttribute("position")
      
        const changePos = ChangePos(posAttr)
      
        const allAng = omega * pi2
      
        const yAng = allAng / heightSegments
        const xAng = allAng / widthSegments
      
        for (let y = 0; y < height; ++y) {
          
            const r0 = Math.sin(y * yAng + phi)
          
            const decay = a * 0.2 + a * (0.5 - Math.abs(y / heightSegments - 0.5))
            for (let x = 0; x < width; ++x) {
              
                const r1 = Math.sin(x * xAng + phi)
              
                changePos((r0 + r1) * decay + 1)
              
              
            }
        }
      
      
        posAttr.needsUpdate = true
    }
}


function ChangePos(attr: BufferAttribute | InterleavedBufferAttribute) {
    let index = 0
  
    const getXYZ = GetXYZ(attr)
    return function (r: number) {
        const p = getXYZ(index)
        p.setLength(r)
      
        attr.setXYZ(index, ...p.toArray())
        index += 1
    }
}


function GetXYZ(attr: BufferAttribute | InterleavedBufferAttribute) {
    return function (ind: number) {
        return new Vector3(attr.getX(ind), attr.getY(ind), attr.getZ(ind))
    }
}


function SetPos(positions: Float32Array) {
    let posNdx = 0
  
  
    return function (lat: number, long: number) {
        const pos = getPoint(lat, long)
        positions.set(pos, posNdx)
        posNdx += 3
        return pos
    }
}

function GetPoint() {
  
    const longHelper = new Object3D()
  
    const latHelper = new Object3D()
  
    const pointHelper = new Object3D()
  
    longHelper.add(latHelper)
    latHelper.add(pointHelper)
    pointHelper.position.z = 1
  
    const temp = new Vector3()
    return function (lat: number, long: number) {
      
        longHelper.rotation.y = long
        latHelper.rotation.x = lat
      
        return pointHelper.getWorldPosition(temp).toArray()
    }
}

function getPoint(lat: number, long: number) {
    const euler = new Euler(lat, long, 0, "YXZ")
    const p = new Vector3(0, 0, 1)
    p.applyEuler(euler)
    return p.toArray()
}
  1. 将波浪球实例化
const geometry = new WaveBall(48, 48);
geometry.wave();
  1. 之后我们也可以在连续渲染方法里将时间传递到 geometry.wave() 里,作为波浪的偏移值。
stage.beforeRender = (time = 0) => {
    geometry.wave(time * 0.002)
}