three.js绘制墙体,通过不规则路径生成墙体,3D墙体绘制

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>

粘贴上面的代码,运行后得到下面这个界面,注意你引用的文件路径

 得到这个图片后,接下来我们就可以进行下一步了

二. 绘制

    绘制会遇到下面几个问题

  1. 鼠标点击拿到对应的三维空间坐标,即二维转三维

  2. 鼠标点击时,实时的生成一条线进行指引

  3. 点击第二下时,生成三维墙体

  4. 生成墙体后,如果不退出绘制,继续绘制,点击右键退出此次绘制

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>

都看到这里了,点个赞呗

猜你喜欢

转载自blog.csdn.net/weixin_44275686/article/details/125845810