0.效果预览,没有加框线所以墙体有点丑,但是哈哈哈,保证是正常的,因为是同一个颜色,视觉上会有差异
效果查看:Three.js实现墙梁板柱的绘制_哔哩哔哩_bilibili
1.准备工作
你看到这篇文章的时候,默认你已经会基础的three.js了,至少,你得会简单的three.js的引入了,当然,不会也不要紧,跟着我的,复制粘贴下面我的代码也可以得到下面这个页面,会的可以跳过这部分直接看第二部分。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试代码</title>
<style>
* {
padding: 0;
margin: 0;
}
</style>
</head>
<body></body>
<script type="module">
// 引入相关包
import * as THREE from "./js/three.module.js";
import { OrbitControls } from "./js/OrbitControls.js";
// 初始化场景,相机,渲染器,控制器,光线
let scene, camera, renderer, controls, light, stats;
function init() {
// 场景初始化
scene = new THREE.Scene();
// 初始化相机
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(100, 190, 300);
// 初始化渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器初始颜色
renderer.setClearColor(new THREE.Color("rgb(200, 200, 200)"));
document.body.appendChild(renderer.domElement);
// 初始化控制器
controls = new OrbitControls(camera, renderer.domElement);
// 使动画循环使用时阻尼或自转 意思是否有惯性
controls.enableDamping = true;
//动态阻尼系数 就是鼠标拖拽旋转灵敏度
controls.dampingFactor = 0.25;
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
controls.autoRotateSpeed = 0.5;
//设置相机距离原点的最远距离
controls.minDistance = 1;
//设置相机距离原点的最远距离
controls.maxDistance = 2000;
//是否开启右键拖拽
controls.enablePan = false;
// 创建网格
const gridHelper = new THREE.GridHelper(
1000,
100,
"rgb(248, 248, 248)",
"rgb(248, 248, 248)",
);
scene.add(gridHelper);
console.log(scene.children);
}
// 动画函数,每帧渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
//窗口变动触发的函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 运行页面
function run() {
init();
animate();
window.onresize = onWindowResize();
}
run();
</script>
</html>
粘贴上面的代码,运行后得到下面这个界面,注意你引用的文件路径
得到这个图片后,接下来我们就可以进行下一步了
二. 绘制
绘制会遇到下面几个问题
-
鼠标点击拿到对应的三维空间坐标,即二维转三维
-
鼠标点击时,实时的生成一条线进行指引
-
点击第二下时,生成三维墙体
-
生成墙体后,如果不退出绘制,继续绘制,点击右键退出此次绘制
2.1.现在来解决第一个问题,获取到三维点
// 获取点
function getCoord(event) {
const container = document.querySelector("canvas");
const adrees = container.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
pointer.x = ((event.clientX - adrees.left) / adrees.width) * 2 - 1;
pointer.y = 1 - ((event.clientY - adrees.top) / adrees.height) * 2;
// 创建平面
const normal = new THREE.Vector3(0, 1, 0);
const planeGround = new THREE.Plane(normal, 0);
// 从相机发出一条射线经过鼠标点击的位置
raycaster.setFromCamera(pointer, camera);
// 拿到该射线
const ray = raycaster.ray;
// 计算相机到射线的对象,可能有多个对象,返回一个数组,按照相机距离远近排列
const intersects = ray.intersectPlane(
planeGround,
new THREE.Vector3(0, 0, 0)
);
// 返回向量
return intersects;
}
此时鼠标点击就可以拿到三维点了
2.2 现在来解决第二个问题,鼠标点击时生成实时的线段
// 鼠标点击时拿到第一个坐标
const canvas = document.querySelector("canvas");
const pointsArr = []; // 存储画墙的坐标
canvas.onmousedown = function (e) {
// 获取到每次点击的坐标
const p = getCoord(e);
// 鼠标按下是左键时,拿到第一个点
if (e.button === 0) {
// 把坐标放到坐标数组中
pointsArr.push([p]);
// 如果有两个点,则生成线段和墙体
if (pointsArr.length >= 2) {
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 将线段添加到场景中
const currentArr = []
currentArr.push(pointsArr[0][0])
currentArr.push(pointsArr[1][0])
lineGeometry.setFromPoints(currentArr)
const line = new THREE.Line(lineGeometry, lineMaterial)
scene.add(line)
}
}
// 鼠标移动时生成实时的线段
canvas.onmousemove = function (e) {
// 获取到第一个点的坐标
const intersects = getCoord(e);
// 鼠标左键未点击时线段的移动状态;
if (scene.getObjectByName("line_move")) {
scene.remove(scene.getObjectByName("line_move"));
}
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 判断是否有初始点
if (pointsArr.length > 0) {
const currentArr = [];
currentArr.push(pointsArr[0][0]);
currentArr.push(new THREE.Vector3(intersects.x, 1, intersects.z));
lineGeometry.setFromPoints(currentArr);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.name = "line_move";
scene.add(line);
}
};
};
此时就得到实时的线了,需要注意的是,代码最好写在初始化函数的下边,不然有可能拿不到canvas这个节点,因为我直接插入到页面中的
2.3 接下来开始改造点击函数onmousedown,加上生成墙体的内容
// 鼠标点击时拿到第一个坐标
const canvas = document.querySelector("canvas");
const pointsArr = []; // 存储画墙的坐标
canvas.onmousedown = function (e) {
// 获取到每次点击的坐标
const p = getCoord(e);
// 鼠标按下是左键时,拿到第一个点
if (e.button === 0) {
// 把坐标放到坐标数组中
pointsArr.push([p]);
// 如果有两个点,则生成线段和墙体
if (pointsArr.length >= 2) {
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 将线段添加到场景中
const currentArr = []
currentArr.push(pointsArr[0][0])
currentArr.push(pointsArr[1][0])
lineGeometry.setFromPoints(currentArr)
const line = new THREE.Line(lineGeometry, lineMaterial)
scene.add(line)
// 画墙开始
// 和并曲线
const start = pointsArr[0][0];
const end = pointsArr[1][0];
const curvePath = new THREE.CurvePath();
const curv3 = new THREE.LineCurve3(
new THREE.Vector3(start.x, start.y, start.z),
new THREE.Vector3(end.x, end.y, end.z)
);
curvePath.add(curv3);
// 设置挤压参数,按路径挤压
const extrudeSettings = {
steps: 200,
bevelEnabled: true,
bevelThickness: 100,
extrudePath: curvePath,
};
// 设置挤压面
const pts = [];
const deepness = 10; // 厚度
const height = 80; // 高度
pts.push(new THREE.Vector2(0, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, 0.5 * deepness));
pts.push(new THREE.Vector2(0, 0.5 * deepness));
// 生成挤压模型
const shape = new THREE.Shape(pts);
const geometry = new THREE.ExtrudeBufferGeometry(
shape,
extrudeSettings
);
const material2 = new THREE.MeshBasicMaterial({
color: "green",
wireframe: false,
});
const mesh = new THREE.Mesh(geometry, material2);
// 将墙体添加到场景中
scene.add(mesh);
// 每当到达三个点的时候 把前面一个点给删除掉,保持两个点
pointsArr.shift();
}
}
// 鼠标移动时生成实时的线段
canvas.onmousemove = function (e) {
// 获取到第一个点的坐标
const intersects = getCoord(e);
// 鼠标左键未点击时线段的移动状态;
if (scene.getObjectByName("line_move")) {
scene.remove(scene.getObjectByName("line_move"));
}
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 判断是否有初始点
if (pointsArr.length > 0) {
const currentArr = [];
currentArr.push(pointsArr[0][0]);
currentArr.push(new THREE.Vector3(intersects.x, 1, intersects.z));
lineGeometry.setFromPoints(currentArr);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.name = "line_move";
scene.add(line);
}
};
};
上面的代码是改造完成的代码,代码到这,你就可以愉快的画墙了,剩下的就是一些细节处理了
2.4 鼠标右键退出绘制,移除线条,继续改造onmousedown,最终代码如下
canvas.onmousedown = function (e) {
// 获取到每次点击的坐标
const p = getCoord(e);
// 鼠标按下是左键时,拿到第一个点
if (e.button === 0) {
// 把坐标放到坐标数组中
pointsArr.push([p]);
// 如果有两个点,则生成线段和墙体
if (pointsArr.length >= 2) {
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 将线段添加到场景中
const currentArr = []
currentArr.push(pointsArr[0][0])
currentArr.push(pointsArr[1][0])
lineGeometry.setFromPoints(currentArr)
const line = new THREE.Line(lineGeometry, lineMaterial)
scene.add(line)
// 画墙开始
// 和并曲线
const start = pointsArr[0][0];
const end = pointsArr[1][0];
const curvePath = new THREE.CurvePath();
const curv3 = new THREE.LineCurve3(
new THREE.Vector3(start.x, start.y, start.z),
new THREE.Vector3(end.x, end.y, end.z)
);
curvePath.add(curv3);
// 设置挤压参数,按路径挤压
const extrudeSettings = {
steps: 200,
bevelEnabled: true,
bevelThickness: 100,
extrudePath: curvePath,
};
// 设置挤压面
const pts = [];
const deepness = 10; // 厚度
const height = 80; // 高度
pts.push(new THREE.Vector2(0, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, 0.5 * deepness));
pts.push(new THREE.Vector2(0, 0.5 * deepness));
// 生成挤压模型
const shape = new THREE.Shape(pts);
const geometry = new THREE.ExtrudeBufferGeometry(
shape,
extrudeSettings
);
const material2 = new THREE.MeshBasicMaterial({
color: "green",
wireframe: false,
});
const mesh = new THREE.Mesh(geometry, material2);
// 讲墙体添加到场景中
scene.add(mesh);
// 每当到达三个点的时候 把前面一个点给删除掉,保持两个点
pointsArr.shift();
}
}
// 鼠标右键点击退出绘制,并回到上一个点
if (e.button === 2) {
// 移除事件
canvas.onmousemove = null
// 移除线段
// 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘
pointsArr.shift();
// 删除线段
let length = scene.children.length - 1;
const children = scene.children;
// 按步骤移除线段
if (scene.children[length].isLine) {
// 只删除最后一条即可,用pop会弹出两
delete scene.children[children.length];
length = scene.children.length - 1;
}
}
// 鼠标移动时生成实时的线段
canvas.onmousemove = function (e) {
// 获取到第一个点的坐标
const intersects = getCoord(e);
// 鼠标左键未点击时线段的移动状态;
if (scene.getObjectByName("line_move")) {
scene.remove(scene.getObjectByName("line_move"));
}
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 判断是否有初始点
if (pointsArr.length > 0) {
const currentArr = [];
currentArr.push(pointsArr[0][0]);
currentArr.push(new THREE.Vector3(intersects.x, 1, intersects.z));
lineGeometry.setFromPoints(currentArr);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.name = "line_move";
scene.add(line);
}
};
};
到这里就结束了
三. 源码
所有的源代码,可直接复制粘贴使用,更改下文件的引用路径即可
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试代码</title>
<style>
* {
padding: 0;
margin: 0;
}
</style>
</head>
<body></body>
<script type="module">
// 引入相关包
import * as THREE from "./js/three.module.js";
import { OrbitControls } from "./js/OrbitControls.js";
// 初始化场景,相机,渲染器,控制器,光线
let scene, camera, renderer, controls, light, stats;
function init() {
// 场景初始化
scene = new THREE.Scene();
// 初始化相机
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(100, 190, 300);
// 初始化渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器初始颜色
renderer.setClearColor(new THREE.Color("rgb(200, 200, 200)"));
document.body.appendChild(renderer.domElement);
// 初始化控制器
controls = new OrbitControls(camera, renderer.domElement);
// 使动画循环使用时阻尼或自转 意思是否有惯性
controls.enableDamping = true;
//动态阻尼系数 就是鼠标拖拽旋转灵敏度
controls.dampingFactor = 0.25;
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
controls.autoRotateSpeed = 0.5;
//设置相机距离原点的最远距离
controls.minDistance = 1;
//设置相机距离原点的最远距离
controls.maxDistance = 2000;
//是否开启右键拖拽
controls.enablePan = false;
// 创建网格
const gridHelper = new THREE.GridHelper(
1000,
100,
"rgb(248, 248, 248)",
"rgb(248, 248, 248)"
);
scene.add(gridHelper);
console.log(scene.children);
}
// 动画函数,每帧渲染
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
//窗口变动触发的函数
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 运行页面
function run() {
init();
animate();
window.onresize = onWindowResize();
}
run();
// 获取点
function getCoord(event) {
const container = document.querySelector("canvas");
const adrees = container.getBoundingClientRect();
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
pointer.x = ((event.clientX - adrees.left) / adrees.width) * 2 - 1;
pointer.y = 1 - ((event.clientY - adrees.top) / adrees.height) * 2;
// 创建平面
const normal = new THREE.Vector3(0, 1, 0);
const planeGround = new THREE.Plane(normal, 0);
// 从相机发出一条射线经过鼠标点击的位置
raycaster.setFromCamera(pointer, camera);
// 拿到该射线
const ray = raycaster.ray;
// 计算相机到射线的对象,可能有多个对象,返回一个数组,按照相机距离远近排列
const intersects = ray.intersectPlane(
planeGround,
new THREE.Vector3(0, 0, 0)
);
// 返回向量
return intersects;
}
// 生成辅助点
function getPoint(intersects) {
const pointLight = new THREE.PointLight(0xff0000, 100, 100);
pointLight.position.set(intersects.x, intersects.y, intersects.z);
scene.add(pointLight);
const sphereSize = 3;
const pointLightHelper = new THREE.PointLightHelper(
pointLight,
sphereSize
);
scene.add(pointLightHelper);
}
// 鼠标点击时拿到第一个坐标
const canvas = document.querySelector("canvas");
const pointsArr = []; // 存储画墙的坐标
canvas.onmousedown = function (e) {
// 获取到每次点击的坐标
const p = getCoord(e);
// 鼠标按下是左键时,拿到第一个点
if (e.button === 0) {
// 把坐标放到坐标数组中
pointsArr.push([p]);
// 如果有两个点,则生成线段和墙体
if (pointsArr.length >= 2) {
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 将线段添加到场景中
const currentArr = []
currentArr.push(pointsArr[0][0])
currentArr.push(pointsArr[1][0])
lineGeometry.setFromPoints(currentArr)
const line = new THREE.Line(lineGeometry, lineMaterial)
scene.add(line)
// 画墙开始
// 和并曲线
const start = pointsArr[0][0];
const end = pointsArr[1][0];
const curvePath = new THREE.CurvePath();
const curv3 = new THREE.LineCurve3(
new THREE.Vector3(start.x, start.y, start.z),
new THREE.Vector3(end.x, end.y, end.z)
);
curvePath.add(curv3);
// 设置挤压参数,按路径挤压
const extrudeSettings = {
steps: 200,
bevelEnabled: true,
bevelThickness: 100,
extrudePath: curvePath,
};
// 设置挤压面
const pts = [];
const deepness = 10; // 厚度
const height = 80; // 高度
pts.push(new THREE.Vector2(0, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, -0.5 * deepness));
pts.push(new THREE.Vector2(-height, 0.5 * deepness));
pts.push(new THREE.Vector2(0, 0.5 * deepness));
// 生成挤压模型
const shape = new THREE.Shape(pts);
const geometry = new THREE.ExtrudeBufferGeometry(
shape,
extrudeSettings
);
const material2 = new THREE.MeshBasicMaterial({
color: "green",
wireframe: false,
});
const mesh = new THREE.Mesh(geometry, material2);
// 讲墙体添加到场景中
scene.add(mesh);
// 每当到达三个点的时候 把前面一个点给删除掉,保持两个点
pointsArr.shift();
}
}
// 鼠标右键点击退出绘制,并回到上一个点
if (e.button === 2) {
// 移除事件
canvas.onmousemove = null
// 移除线段
// 删除数组中的元素,否则的话再次重绘会链接之前的点接着重绘
pointsArr.shift();
// 删除线段
let length = scene.children.length - 1;
const children = scene.children;
// 按步骤移除线段
if (scene.children[length].isLine) {
// 只删除最后一条即可,用pop会弹出两
delete scene.children[children.length];
length = scene.children.length - 1;
}
}
// 鼠标移动时生成实时的线段
canvas.onmousemove = function (e) {
// 获取到第一个点的坐标
const intersects = getCoord(e);
// 鼠标左键未点击时线段的移动状态;
if (scene.getObjectByName("line_move")) {
scene.remove(scene.getObjectByName("line_move"));
}
// 创建线段
const lineGeometry = new THREE.BufferGeometry();
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
});
// 判断是否有初始点
if (pointsArr.length > 0) {
const currentArr = [];
currentArr.push(pointsArr[0][0]);
currentArr.push(new THREE.Vector3(intersects.x, 1, intersects.z));
lineGeometry.setFromPoints(currentArr);
const line = new THREE.Line(lineGeometry, lineMaterial);
line.name = "line_move";
scene.add(line);
}
};
};
</script>
</html>
都看到这里了,点个赞呗