空间坐标点绘制多边形,实际上可以理解为是由 “点” 到 “线” 到 “面” 的一个过程。将空间坐标点通过THREE.Shape
绘制多条线并闭合而得到一个封闭的二维形状平面对象,使用THREE.ShapeGeometry
将Shape
对象转换为Geometry
对象添加Mesh
,最终得到我们想要的多边形几何体。要得到一个围栏就需要添加“墙体”,“墙体”则是通过THREE.BoxGeometry
计算偏移角度绘制多个几何体得到。
使用技术简介
实现一个多边形围栏主要用到THREE.Shape
、THREE.ShapeGeometry
和THREE.BoxGeometry
。
THREE.Shape
Shape
是用于创建平面形状的类。Shape
可以用来创建一个简单的二维形状,然后使用ShapeGeometry
将其转换为可呈现的封闭形状。它可以和ExtrudeGeometry
、ShapeGeometry
一起使用,获取点,或者获取三角面。
常用属性
- uuid: 该类所创建的实例的UUID。自动被指定的,因此它不应当被编辑、更改
- holes: 表示形状内部的零或多个孔的数组。即表示包含所有内部空洞(也是Shape对象)的数组。默认值是一个空数组。
- autoClose: 表示路径是否自动关闭的属性。默认false。
常用方法
- moveTo: 将绘图点的起点移动到一个新的位置(x,y)并在Shape路径的路径中创建一个新的点。
- lineTo:向Shape路径中添加一条直线,从当前点到新点(x,y)。
THREE.ShapeGeometry
ShapeGeometry
是用于从Shape
对象创建几何体的类。Shape
对象可以用来创建二维形状,这些形状可以用于创建网格(Mesh)或线框(Line)。
THREE.BoxGeometry
BoxGeometry
是用于创建三维立方体的几何体。它允许用户指定立方体的宽度、高度、深度以及这三个方向上的分段数,通过这些参数可以灵活地调整立方体的尺寸和细节级别。BoxGeometry
的构造函数通常包含以下参数:
- width:立方体的宽度。
- height:立方体的高度。
- depth:立方体的深度。
- widthSegments:在X轴方向上将立方体的面分成的段数。
- heightSegments:在Y轴方向上将立方体的面分成的段数。
- depthSegments:在Z轴方向上将立方体的面分成的段数。
注意:围栏是一个闭环,初始数据也是实现围栏的基础和关键,必须为一个闭环数据,即数组第一条数据和最后条数据相同。
点数据处理
在three.js中,长度总是从(0, 0, 0)到(x, y, z)的Euclidean distance(欧几里德距离,即直线距离),方向也是从(0, 0, 0)到(x, y, z)的方向。所以我们需要先转换一下。
const points = [
[5, 0, 2],
[8, 0, 2],
[7, 0, 3],
[6, 0, 3],
[5, 0, 2]
];
const pointVector = [];
for (let i = 0; i < points.length; i++) {
const item = points[i];
pointVector.push(new THREE.Vector3(item[0], item[1], item[2]));
}
将点绘制成一个二维平面
将转换后的空间坐标,通过Shape moveTo
和lineTo
绘制多条线,多条线收尾相连,从而得到一个二维平面。
const shape = new THREE.Shape();
shape.moveTo(pointVector[0].x, pointVector[0].z);
for (let i = 1; i < pointVector.length; i++) {
shape.lineTo(pointVector[i].x, pointVector[i].z);
}
shape.autoClose = true;
将二维平面转成多面几何体
将Shape
得到的二维平面转成Geometry
并设置网格样式。
// 从一个或多个路径形状中创建一个单面多边形几何体。
const shapeGeometry = new THREE.ShapeGeometry(shape, 25);
const shapeMaterial = new THREE.MeshBasicMaterial({
color: 0xFF0018,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5
});
const shapeMesh = new THREE.Mesh(shapeGeometry, shapeMaterial);
shapeMesh.rotateX(Math.PI / 2);
绘制围栏
围栏可以理解为给一个房间添加“墙体”。通过BoxGeometry
添加多个几何体来代实现多面墙,计算墙体依据多边形的边的偏移角度来实现,从而实现拼凑出围栏的效果。
// 墙体数据处理
const wallArr = [...pointArr];
for (let i = 0; i < wallArr.length; i++) {
if (i !== wallArr.length - 1) {
let params = {
startX: wallArr[i].x,
endX: wallArr[i + 1].x,
startZ: wallArr[i].z,
endZ: wallArr[i + 1].z
};
const wallHeight = 1; // 墙体高度,默认1
// 计算墙体宽度
const lens = Math.sqrt(Math.pow((Number(params.endZ) - Number(params.startZ)), 2) + Math.pow((Number(params.endX) - Number(params.startX)), 2));
// 绘制网格模型,设置墙体样式
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(wall);
texture.wrapS = THREE.RepeatWrapping; //水平方向如何包裹
texture.wrapT = THREE.RepeatWrapping; // 垂直方向如何包裹
// uv两个方向纹理重复数量、看板中重复数量
texture.repeat.set(10, 1);
// 设置偏移 纹理在单次重复时,从一开始将分别在U、V方向上偏移多少。 这个值的范围通常在0.0之间1.0
texture.offset = new THREE.Vector2(0, 0);
// 绘制墙体
const box = new THREE.BoxGeometry(lens, wallHeight, 0); //- 墙体参数 墙体高度1
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 1
});
const mesh = new THREE.Mesh(box, material);
// 设置单面墙体位置
const posx = (params.endX + params.startX) / 2;
const posz = (params.endZ + params.startZ) / 2;
mesh.position.set(posx, points[0][1] + (wallHeight / 2), posz);
// 设置墙体旋转角度
const rotate = -Math.atan2((params.endZ - params.startZ), (params.endX - params.startX));
mesh.rotation.y = rotate;
// 将墙体添加到场景中
scene.add(mesh);
}
}
完整实例代码
注意:我这里的数据是一个闭环,如果你们的数据不是闭环需要处理一下,否者平面墙体都没有闭合就达不到围栏效果。
import React, { useRef, useEffect } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const wall = require('@/static/image/wall.png');
let renderer, controls, scene, camera;
// three.js绘制多边形围栏
const Draw3DHollowCylinder = () => {
const box = useRef(); // canvas盒子
// 渲染动画
function renderFn() {
requestAnimationFrame(renderFn);
// 用相机渲染一个场景
renderer.render(scene, camera);
}
/**
* 绘制墙体
* @param {墙体坐标点} params
* @param {区域位于空间y轴位置} yHei
* @param {几何图形组} group
*/
function drawPloygonWall(params, yHei, group) {
const wallHeight = 1; // 墙体高度
// 长度
const lens = Math.sqrt(Math.pow((Number(params.endZ) - Number(params.startZ)), 2) + Math.pow((Number(params.endX) - Number(params.startX)), 2));
// 绘制网格模型,设置墙体样式
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load(wall);
texture.wrapS = THREE.RepeatWrapping; //水平方向如何包裹
texture.wrapT = THREE.RepeatWrapping; // 垂直方向如何包裹
// uv两个方向纹理重复数量、看板中重复数量
texture.repeat.set(10, 1);
// 设置偏移 纹理在单次重复时,从一开始将分别在U、V方向上偏移多少。 这个值的范围通常在0.0之间1.0
texture.offset = new THREE.Vector2(0, 0);
// 设置墙体参数,深度为0
const box = new THREE.BoxGeometry(lens, wallHeight, 0);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 1
});
const mesh = new THREE.Mesh(box, material);
// 设置单面墙体位置
const posx = (params.endX + params.startX) / 2;
const posz = (params.endZ + params.startZ) / 2;
mesh.position.set(posx, yHei + (wallHeight / 2), posz);
// 设置墙体旋转角度
const rotate = -Math.atan2((params.endZ - params.startZ), (params.endX - params.startX));
mesh.rotation.y = rotate;
// 将墙体添加到组中
group.add(mesh);
}
useEffect(() => {
if (scene) {
const points = [
[5, 0, 2],
[8, 0, 2],
[7, 0, 3],
[6, 0, 3],
[5, 0, 2]
];
const pointArr = [];
for (let i = 0; i < points.length; i++) {
const item = points[i];
pointArr.push(new THREE.Vector3(item[0], item[1], item[2]));
}
// 点绘制成线,再到二维平面
const shape = new THREE.Shape();
shape.moveTo(pointArr[0].x, pointArr[0].z);
for (let i = 1; i < pointArr.length; i++) {
shape.lineTo(pointArr[i].x, pointArr[i].z);
}
shape.autoClose = true; // 设置路径自动关闭
// 从一个或多个路径形状中创建一个多边形几何体。
const shapeGeometry = new THREE.ShapeGeometry(shape, 25);
const shapeMaterial = new THREE.MeshBasicMaterial({
color: 0xFF0018,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5
});
const shapeMesh = new THREE.Mesh(shapeGeometry, shapeMaterial);
shapeMesh.rotateX(Math.PI / 2);
const group = new THREE.Group();
// 墙体数据处理
const wallArr = [...pointArr];
for (let i = 0; i < wallArr.length; i++) {
if (i !== wallArr.length - 1) {
let params = {
startX: wallArr[i].x,
endX: wallArr[i + 1].x,
startZ: wallArr[i].z,
endZ: wallArr[i + 1].z
};
// 绘制墙体
drawPloygonWall(params, points[0][1], group);
}
}
group.add(shapeMesh);
scene.add(group);
}
}, [scene]);
// 初始化环境、灯光、相机、渲染器
useEffect(() => {
scene = new THREE.Scene();
// 添加光源
const ambitlight = new THREE.AmbientLight(0x404040);
scene.add(ambitlight)
const sunlight = new THREE.DirectionalLight(0xffffff);
sunlight.position.set(-20, 1, 1);
scene.add(sunlight);
// 获取宽高设置相机和渲染区域大小
const width = box.current.offsetWidth;
const height = box.current.offsetHeight;
const k = width / height;
// 投影相机
camera = new THREE.PerspectiveCamera(75, k, 0.1, 1000);
camera.position.set(1, 0, 25);
camera.lookAt(scene.position);
// 创建一个webGL对象
renderer = new THREE.WebGLRenderer({
//增加下面两个属性,可以抗锯齿
antialias: true,
alpha: true
});
renderer.setSize(width, height); // 设置渲染区域尺寸
renderer.setClearColor(0x000000, 1); // 设置颜色透明度
// 首先渲染器开启阴影
renderer.shadowMap.enabled = true;
box.current.appendChild(renderer.domElement);
// 监听鼠标事件
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;//设置为true则启用阻尼(惯性),默认false
controls.dampingFactor = 0.05;//值越小阻尼效果越强
// 渲染
renderFn();
}, []);
return <div className='ui_container_box'>
three.js绘制3D多边形区域。
<div style={{ width: '100%', height: '100%' }} ref={box}></div>
</div>;
}
export default Draw3DHollowCylinder;