几何体
几何体是描述物体形状的数据结构,可以用来描述物体的表面、轮廓和体积,是物体不可缺少的一部分。
如果你有接触建模或是见过模型网格状你会知道几何体都是许多许多个三角形组成的

因为三角形是最简单的多边形,他可以组成所有的形状。如四边形是由两个三角形组成,五边形是由三个三角形组成,六边形...等等。
三角形越多,物体也就越精细。
threejs封装了很多有意思的几何体供我们使用,如立方体、胶囊、圆形、管道等。
详情在这里,有参数调试 BoxGeometry
我们详细讲一讲在以往的某几篇文章中有接触到的 BufferGeometry
BufferGeometry
BufferGeometry 是所有几何体的基类,他可以自定义物体的顶点、颜色、法向量和UV坐标。
顶点是描述 3D 对象形状的最基本单位,如画一个正方形面,只需要6个顶点就可以
js
function generateSquare() {
let square = new THREE.BufferGeometry();
let attributes: number[] = [];
let colors: number[] = [];
attributes = attributes.concat([
-1.0, -1.0, 0, // 左下
1.0, -1.0, 0, // 右下
1.0, 1.0, 0, // 右上
1.0, 1.0, 0, // 右上
-1.0, 1.0, 0, // 左上
-1.0, -1.0, 0 // 左下
]);
var posAttribute = new THREE.BufferAttribute(new Float32Array(attributes), 3);
square.setAttribute('position', posAttribute);
let mesh = new THREE.Mesh(square, new THREE.MeshBasicMaterial())
scene.add(mesh);
}

Float32Array 类是存储浮点数的数组类型,拥有高精度的特性,常用于处理二进制数据。 Float32Array;
BufferAttribute 用于存储几何体相关属性,可以高效的向 GPU 传递数据。
顶点顺序
顶点绘制顺序是有讲究的,逆时针绘制是朝向观察者正面,顺时针绘制时朝向观察者背面。

可以修改上面顶点查看
js
attributes = attributes.concat([
1.0, 1.0, 0, // 右上
1.0, -1.0, 0, // 右下
-1.0, -1.0, 0, // 左下
-1.0, 1.0, 0, // 左上
1.0, 1.0, 0, // 右上
-1.0, -1.0, 0 // 左下
]);

在 openGL 中的"面剔除"中有解释:观察者观察正方体时,最多可以看 3 个面,观察不到的另外 3 个面可以以某种方式进行隐藏,这种方式就是通过顶点绘制顺序进行判断。
绘制立方体
绘制立方体也是相同的方式,准备18 * 6 个顶点。
js
attributes = attributes.concat([
1, 1, 1, 1, -1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, //(左)
-1, 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, 1, -1, 1, 1,//(右)
-1, 1, -1, -1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, //(上)
-1, -1, 1, -1, -1, -1, 1, -1, 1, -1, -1, -1, 1, -1, -1, 1, -1, 1,//(下)
-1, 1, 1, -1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, //(后)
1, 1, -1, 1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1 //(前)
]);

官方示例详解
官方示例有一个用 bufferGeometry生成粒子效果 我们只关注顶点观察代码
原理:在xyz轴范围为-400 至 400的立方体内随机找到一个P点, 以P点为中心范围为 12 的立方体内随机找到 三个顶点组成三角形。
js
const triangles = 16000;
const geometry = new THREE.BufferGeometry();
const positions = [];
const n = 800, n2 = n / 2; //
const d = 12, d2 = d / 2; // 三角形的顶点 在一个以P点为中心范围为 12 的立方体内
for (let i = 0; i < triangles; i++) {
// 定义P点坐标
const x = Math.random() * n - n2;
const y = Math.random() * n - n2;
const z = Math.random() * n - n2;
// 以P中心点为中心,设置三个随机顶点组成三角形
const ax = x + Math.random() * d - d2;
const ay = y + Math.random() * d - d2;
const az = z + Math.random() * d - d2;
const bx = x + Math.random() * d - d2;
const by = y + Math.random() * d - d2;
const bz = z + Math.random() * d - d2;
const cx = x + Math.random() * d - d2;
const cy = y + Math.random() * d - d2;
const cz = z + Math.random() * d - d2;
positions.push(ax, ay, az);
positions.push(bx, by, bz);
positions.push(cx, cy, cz);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
side: THREE.DoubleSide,
});
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

只要能应用好顶点位置,可以画非常好看的图案.
顶点颜色
光有三角形感觉缺了点什么,我们给他赋予一些颜色吧。
在自定义几何体中,我们需要给每个顶点设置一个颜色值。
js
1. 定义一个Color变量与一个颜色数组。
const colors = [];
const color = new THREE.Color();
2. 给每个顶点设置随机颜色
color.setRGB(Math.random(),Math.random(),Math.random());
const alpha = Math.random();
colors.push(color.r, color.g, color.b, alpha);
colors.push(color.r, color.g, color.b, alpha);
colors.push(color.r, color.g, color.b, alpha);
3. 将数据添加到 BufferAttribute
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
// 等同于 geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
4. 添加材质属性 vertexColors;
const material = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
vertexColors: true,
side: THREE.DoubleSide,
});

法向量
当你使用MeshBasicMaterial材质时,你会发现物体不会受到光照影响,那光照影响的依据是什么呢?
答案就是法向量!
法向量是三维向量,它表示三维空间中一个表面的方向。它始终与表面垂直,并且其长度为 1。法向量可以用于计算光照、阴影和反射。
- 计算光照:光照在表面上是如何分布的取决于表面的法向量。如果光线与法向量垂直,则表面将被完全照亮。如果光线与法向量平行,则表面将完全被阴影。
- 计算阴影:阴影是由于光线被物体阻挡而产生的。阴影的形状取决于物体表面的法向量。
- 计算反射:反射是光线从表面反射回来的现象。反射的角度取决于光线入射角和表面法向量之间的关系。
具体的会在后面的材质文章中详解。

js
1. 设置向量点pA、pB、pC,向量cb、ab
const pA = new THREE.Vector3();
const pB = new THREE.Vector3();
const pC = new THREE.Vector3();
const cb = new THREE.Vector3();
const ab = new THREE.Vector3();
2. 赋值三角形的顶点给pA、pB、pC,向量cb、ab,求法向量
pA.set(ax, ay, az);
pB.set(bx, by, bz);
pC.set(cx, cy, cz);
cb.subVectors(pC, pB); 向量bc
ab.subVectors(pA, pB); 向量ba
cb.cross(ab); // 叉乘,求平面的法向量
cb.normalize(); // 归一化 (0 ~ 1)
const nx = cb.x;
const ny = cb.y;
const nz = cb.z;
normals.push(nx, ny, nz);
normals.push(nx, ny, nz);
normals.push(nx, ny, nz);
3. 将数据添加到 BufferAttribute
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
4. 修改高亮材质 MeshPhongMaterial
const material = new THREE.MeshPhongMaterial({
color: 0xFFFFFF,
vertexColors: true,
side: THREE.DoubleSide,
});

可以看到颜色变得更加鲜艳了,那是因为添加了法线受到了光照的影响。
VertexNormalsHelper 法线辅助对象
js
const helper = new VertexNormalsHelper(mesh, 50, 0xfff000);
scene.add(helper);
完整代码
js
<template>
<div ref="dom"></div>
</template>
<script lang="ts" setup>
import * as THREE from "three";
import { Ref, onMounted, ref } from "vue";
import tApi from "@/utils/three"
import { Vector3 } from "three";
import dat from 'dat.gui'
import { VertexNormalsHelper } from "three/examples/jsm/helpers/VertexNormalsHelper";
let { SpotLight, PointLight, PerspectiveCamera, DirectionalLightHelper, SpotLightHelper, MeshLambertMaterial, PointLightHelper, DirectionalLight, Mesh, BoxGeometry, MeshBasicMaterial, MeshStandardMaterial, WebGLRenderer, Scene, AmbientLight } = THREE;
let camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
let renderer = new WebGLRenderer();
let scene = new Scene();
let light = new AmbientLight(0xFFFFFF, 1);
let dom: Ref<HTMLElement | null> = ref(null);
tApi.addOrbitControls(camera, renderer.domElement);
scene.add(light);
let directionLight = new DirectionalLight(0xff0000, 3,);
directionLight.position.set(5, 5, -5);
scene.add(directionLight);
const directionLightHelper = new DirectionalLightHelper(directionLight);
scene.add(directionLightHelper);
function generateSquare() {
const triangles = 12000;
const geometry = new THREE.BufferGeometry();
const normals = [];
const positions = [];
const colors = [];
const color = new THREE.Color();
const pA = new THREE.Vector3();
const pB = new THREE.Vector3();
const pC = new THREE.Vector3();
const cb = new THREE.Vector3();
const ab = new THREE.Vector3();
const n = 800, n2 = n / 2;
const d = 12, d2 = d / 2;
for (let i = 0; i < triangles; i++) {
const x = Math.random() * n - n2;
const y = Math.random() * n - n2;
const z = Math.random() * n - n2;
const ax = x + Math.random() * d - d2;
const ay = y + Math.random() * d - d2;
const az = z + Math.random() * d - d2;
const bx = x + Math.random() * d - d2;
const by = y + Math.random() * d - d2;
const bz = z + Math.random() * d - d2;
const cx = x + Math.random() * d - d2;
const cy = y + Math.random() * d - d2;
const cz = z + Math.random() * d - d2;
positions.push(ax, ay, az);
positions.push(bx, by, bz);
positions.push(cx, cy, cz);
pA.set(ax, ay, az);
pB.set(bx, by, bz);
pC.set(cx, cy, cz);
cb.subVectors(pC, pB);
ab.subVectors(pA, pB);
cb.cross(ab);
cb.normalize();
const nx = cb.x;
const ny = cb.y;
const nz = cb.z;
normals.push(nx, ny, nz);
normals.push(nx, ny, nz);
normals.push(nx, ny, nz);
color.setRGB(Math.random(), Math.random(), Math.random());
const alpha = Math.random();
colors.push(color.r, color.g, color.b, alpha);
colors.push(color.r, color.g, color.b, alpha);
colors.push(color.r, color.g, color.b, alpha);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geometry.computeBoundingSphere();
const material = new THREE.MeshPhongMaterial({
color: 0xFFFFFF,
vertexColors: true,
side: THREE.DoubleSide,
});
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
/* const helper = new VertexNormalsHelper(mesh, 50, 0xfff000);
scene.add(helper); */
}
generateSquare();
camera.position.set(0, 0, -1600);
camera.lookAt(new Vector3(0, 0, 0));
renderer.setSize(window.innerWidth, window.innerHeight);
let animate = () => {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
let setSceneAdapt = () => {
let width = window.innerWidth, height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix()
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio)
}
onMounted(() => {
if (dom.value) {
dom.value.appendChild(renderer.domElement);
animate();
}
window.addEventListener("resize", setSceneAdapt)
})
</script>
<style></style>