Record--I am working on the front-end site (three.js)

Here I will share with you some of the knowledge I have summarized on the Internet, I hope it will be helpful to everyone

After I came into contact with Three.js some time ago, I tried to use him to load some models. The first experience of three.js was a rough understanding of the related use of three.js, and I wrote the first article. But after that, I still don’t know a lot about it. As a developer who doesn’t know how to model and has no contact with relevant business needs, can’t there be nothing without a model? Therefore, I think it is necessary to try to build the scene without a model in order to deepen the understanding.

I am working on the front-end construction site, and the final result is as shown in the figure below. preview address

As can be seen from the figure, the entire screen consists of the following

  1. ground
  2. house
  3. The scene can be built step by step under the sky.

1. Preparation

My environment here is vue3 + vitethat the three.js library must be installed after the basic environment is ready, and you can continue after the installation is complete. Next, build the basic structure of the page

<template>
  <div class="current-page">
    <canvas
      id="draw"
      style="border: 1px solid; background-color: #000"
    ></canvas>
  </div>
</template>
<script setup >
  import * as THREE from "three";
  import { onMounted } from "vue";
  import { OrbitControls } from "@/controls/OrbitControls";//引入轨道控制器
  //canvas的大小
  const width = 500;
  const height = 500;
  //建筑的长宽
  const baseWidth = 40;
  const baseLength = 60;
  let scene = null; //场景
  let camera = null; //相机
  let canvas = null; //用作渲染的canvas
  let renderer = null; //渲染器
  const group = new THREE.Group();//用于将建筑物的各个零件组合起来
  onMounted(()=>{
      //***一些代码
  })
</script>

Since the image needs to be displayed in the page canvas, it needs to be onMountedobtained in the life cycle. Next, create several elements to build the scene: 相机(camera), 场景(scene), 渲染器(renderer), 灯光(light), onMountedadd the following code in:

  canvas = document.querySelector("#draw");
  //创建场景
  scene = new THREE.Scene();
  //创建一个透视相机
  camera = new THREE.PerspectiveCamera(125, width / height, 1, 2000);
  //设置相机位置
  camera.position.set(-30, 30, 50);
  //创建环境光
  const hjLight = new THREE.AmbientLight(0xffffff);
  //添加环境光至场景
  scene.add(hjLight);
  
  
  
  //添加房子的group到场景中
  scene.add(group);
  //添加轨道控制器
  const controls = new OrbitControls(camera, canvas);
  //渲染器
  renderer = new THREE.WebGLRenderer({
    canvas,//传入要渲染的canvas,相关参数可以看文档
    antialias: true,//抗锯齿
    alpha: true,
  });
  //设置渲染器大小
  renderer.setSize(width, height);
  
   //渲染器开始渲染
  renderer.render(scene, camera);
   //执行
  function animate() {
    controls.update();
    renderer.render(scene, camera);
  }
  animate()

Through the above code, we already have several elements, and we can officially start below.

2. Create the ground

CircleGeometryThe built-in geometry of three.js is used here on the ground . There is no mandatory requirement here, as long as it conforms to the appearance. Add a method createGroundbelow, just find a picture as the material of the ground, then call it, and then you can see a circular ground with a radius of 500

//创建地面
function createGround() {
  //导入材质
  const groundTexture =  new THREE.TextureLoader().load("/grass.webp");
  groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
  groundTexture.repeat.set(100, 100);
  const ground = new THREE.CircleGeometry(500, 100);
  const groundMaterial = new THREE.MeshLambertMaterial({
    side: THREE.DoubleSide,
    map: groundTexture,
    // transparent: true,
    // opacity: 0.2,
  });
  
  const groundMesh = new THREE.Mesh(ground, groundMaterial);
  groundMesh.name = "地面";//设置name属性
  groundMesh.rotateX(-Math.PI / 2);//旋转用于呈现一个水平的地面
  scene.add(groundMesh);

After adjusting the angle and size, you can see the following picture:

3. Start building the house

1. Create the floor

Create the floor here, using the built-in BoxGeometrygeometry here. Add createFloormethod, as follows:

//创建地板,可以理解为地基
function createFloor() {
  const texture = new THREE.TextureLoader().load("/img/wood.jpg");
  //设置地板大小,由于后面将要生成墙体存在设置为1的厚度,因此这里对地板的x,z均-2
  const floor = new THREE.BoxGeometry(baseWidth - 2, 1, baseLength - 2);
  const material = new THREE.MeshPhongMaterial({ map: texture });
  const mesh = new THREE.Mesh(floor, material);
  mesh.position.set(0, 1, 0);
  mesh.name = "地板";
  group.add(mesh);
}

After calling the above method, the page is as shown below

2. Create the left and right walls

这里开始左右两边的规则墙体,使用的也是内置的BoxGeometry几何体。添加 createWall方法,这个方法返回创建的墙体

function createWall() {
  const wallTexture = new THREE.TextureLoader().load("/img/wall1.jpg");
  const wall = new THREE.BoxGeometry(baseLength, 20, 1);
  const wallMaterial = new THREE.MeshPhongMaterial({
    map: wallTexture,
  });
  //墙体的网格
  const wallMesh = new THREE.Mesh(wall, wallMaterial);
  return wallMesh;
}

接下来调用方法生成第一面墙

let leftWall = createWall()
leftWall.name = '左侧的墙'
group.add(leftWall)

 调用后画面中应该如下图

 很显然这与我们的预期不符,下面设置墙体的位置并且让左侧的墙旋转90度,注意rotateY的参数为弧度。

 leftWall.rotateY(Math.PI / 2);
 leftWall.position.set(-baseWidth / 2, 10, 0);

调整完成后如下图

 接下来我们如法炮制右侧的墙,可以重新调用一次方法或者使用几何体对象三的clone方法。这里使用clone

  const rightWall = leftWall.clone();
  rightWall.position.set(baseWidth / 2, 10, 0);
  rightWall.name = "右侧的墙";
  group.add(rightWall);

 调整后如下图

3、创建前后的墙体

 规则的,这个时候使用创建前面墙的几何体便不行了,查阅文档后得知可以使用挤压缓冲几何体(ExtrudeGeometry),官网的描述如图

 如何使用呢,构造器

文档中清晰的表明了这个类有两个参数,一个是 形状或者包含形状的数组 和配置选项。那么到这里我产生了疑问,什么是形状的数组呢,接着查看文档找到了shape这个类

 实例代码中一眼望去熟悉的api映入眼帘,因此大概能想象出用法。 由于前后两堵墙大致的形状都是相同的,因此写一个返回形状数组的方法 genwallShape

function genwallShape() {
  const shape = new THREE.Shape();
  let height = 20;//墙的高度
  shape.moveTo(0, 0); //起点
  shape.lineTo(0, height); //墙体高度
  shape.lineTo(baseWidth / 2 - 1, height + 5); //墙体顶点
  shape.lineTo(baseWidth / 2 - 1, height + 6); //墙体顶点
  shape.lineTo(baseWidth / 2 + 1, height + 6); //墙体顶点
  shape.lineTo(baseWidth / 2 + 1, height + 5); //墙体顶点
  shape.lineTo(baseWidth, height);
  shape.lineTo(baseWidth, 0);
  shape.lineTo(0, 0);
  return { shape };
}

生成点的数组的的方法有了,接下来写一个生成不规则墙体的方法createIrregularWall

//创建不规则墙体
function createIrregularWall(shape, position) {
  const extrudeSettings = {
    depth: 1,//定义深度,由于挤压几何体的点位都是x,y坐标组成的二位平面,这个参数定义向z轴的延展长度,即为墙的厚度
    bevelEnabled: false,
  };
  const wallTexture = new THREE.TextureLoader().load("/img/wall1.jpg");
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
  wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping;
  wallTexture.repeat.set(0.05, 0.05);
  const material = new THREE.MeshPhongMaterial({ map: wallTexture });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(...position);
  group.add(mesh);
  return mesh;
}

点位有了、方法也有了,下面开始创建后墙,添加一个对应的方法

//创建不带门的不规则墙体
function createNoDoorWall() {
  let { shape } = genwallShape();
  let mesh = createIrregularWall(shape, [-baseWidth / 2, 0, -baseLength / 2]);
  mesh.name = "带门的墙对面的墙";
}

调用后如图

 问题又来了,怎么在墙上打洞呢,还是在文档中找到了答案

shape上有一个属性表示了孔洞了,接下来就好办了

function createDoorWall() {
  let { shape } = genwallShape();
  const door = new THREE.Path();
  //门的位置
  door.moveTo(baseWidth / 2 + 5, 0);
  door.lineTo(baseWidth / 2 + 5, 16);
  door.lineTo(baseWidth / 2 - 5, 16);
  door.lineTo(baseWidth / 2 - 5, 0);
  door.lineTo(baseWidth / 2 + 5, 0);
  // 形状上的孔洞
  shape.holes.push(door);
  let mesh = createIrregularWall(shape, [
    -baseWidth / 2,
    0,
    baseLength / 2 - 1,
  ]);
  mesh.name = "带门的墙";
}

调用后如下

 可以看到门的形状已经出来了

4、创建屋顶

这里我们开始创建屋顶,首先求出屋顶的宽度,也就是我们要创建的几何体的z轴的延展

function createRoof() {

  //屋顶宽
  let width = Math.sqrt((baseWidth / 2) ** 2 + 5 ** 2) + 5;//+5让有一点屋檐的效果
  const geometry = new THREE.BoxGeometry(baseLength + 2, width, 1);
  const texture = new THREE.TextureLoader().load("/img/tile.jpg");
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  texture.repeat.set(2, 2);
  const material = new THREE.MeshPhongMaterial({ map: texture });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.rotateZ(THREE.MathUtils.degToRad(75));
  mesh.rotateY(-Math.PI / 2);
  mesh.position.set(baseWidth / 3 - 1, 22, 0);
  mesh.name = "右屋顶";
  group.add(mesh);
  return { roof: mesh, width };

}

方法执行后如下图,我们有了一个右边的屋顶

 如法炮制,再来一个左边的屋顶

 let { roof, width } = createRoof();
  return
  let leftRoof = roof.clone();
  leftRoof.rotateX(THREE.MathUtils.degToRad(30));
  leftRoof.position.set(-baseWidth / 3 + 1, 22, 0);
  leftRoof.name = "左屋顶";
  group.add(leftRoof);

随后可以在画面中看到如下图,我们的房子,哦不,准确的说是仓库已经出来了。。。

5、创建门

然后我们开始创建门,门的创建也是用的内置的BoxGeometry几何体。 添加一个createDoor方法,如下

function createDoor() {
   //纹理贴图
  const texture = new THREE.TextureLoader().load("/img/door.jpg");
  //门的大小、尺寸
  const door = new THREE.BoxGeometry(10, 15, 0.5);
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    transparent: true,
    opacity: 1,
  });
  const doorMesh = new THREE.Mesh(door, material);
  doorMesh.name = "门";
  doorMesh.position.x = 5;
  group.add(doorGroup);

}

调用后即可看到原本的门洞中出现了一扇门,如下图

6、为场景添加点击

Next, I want to make a click to open the door, so first of all, I need to get which objects the mouse clicks. Coincidentally, three.js provides us with a Raycasterclass to detect which objects the ray touches. Add the following code

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

function onPointerMove(event) {
  // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
  pointer.x = (event.clientX / width) * 2 - 1;
  pointer.y = -(event.clientY / height) * 2 + 1;
}

Then add click event listener to canvas

 canvas.addEventListener(
    "click",
    () => {
      const intersects = raycaster.intersectObjects(scene.children);
      console.log(intersects[0]);
      console.log("点击了", intersects[0]?.object?.name);
   
    },
    false
  );

Then we click on the scene, and the object we click can be clearly printed in the console. Explanation: intersectObjectsThe method will return an array of all objects that the ray passes through, and the 0th bit of the array is the object closest to the clicked area, so it can be regarded as the clicked object.

Now that we know which object is clicked, let's add the animation effect of the door

7. Add door closing and door opening animation effects

Adjust the method of the previous section, as follows, and trigger it when the clicked object is a door.

  canvas.addEventListener(
    "click",
    () => {
      const intersects = raycaster.intersectObjects(scene.children);
      console.log(intersects[0]);
      console.log("点击了", intersects[0]?.object?.name);
      if (intersects[0]?.object?.name == "门") {
        // console.log(intersects[0].object.parent.rotation.y);
        let speed = 0.05;
        //再次点击关门
        if (intersects[0].object.parent.rotation.y <= -2.5) {
          // console.log("关门");
          let a = setInterval(() => {
            if (intersects[0].object.parent.rotation.y >= 0) {
              intersects[0].object.parent.rotation.y = 0;
              clearInterval(a);
              return;
            }

            intersects[0].object.parent.rotation.y += speed;
          }, 1000 / 60);
        } else {
          // console.log("开门");
          let a = setInterval(() => {
            if (intersects[0].object.parent.rotation.y <= -2.5) {
              clearInterval(a);
              return;
            }

            intersects[0].object.parent.rotation.y -= speed;
          }, 1000 / 60);
        }
      }
    },
    false
  );

Will this work correctly? Of course not, the above code makes the door rotate on the y-axis, but in three.js, the rotation axis of the object is the center of the object, so we need to change the rotation axis of the door to make it visually appear with the rotation center Changes, let’s modify createDoorthe method below, as follows,

//创建门
function createDoor() {
  const texture = new THREE.TextureLoader().load("/img/door.jpg");
  const door = new THREE.BoxGeometry(10, 15, 0.5);
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    transparent: true,
    opacity: 1,
  });
  const doorMesh = new THREE.Mesh(door, material);
  // doorMesh.rotateY(Math.PI / 2);
  // doorMesh.position.set(-baseLength / 2, 7, 0);

  doorMesh.name = "门";
  
  //以下代码做出了更改
  const doorGroup = new THREE.Group();//添加一个门的父级
  doorGroup.name = "门的包裹";
  doorGroup.position.set(-5, 8, baseLength / 2);//通过父级来改变门的旋转轴
  //现在这个是相对于父级
  doorMesh.position.x = 5;
  doorGroup.add(doorMesh);
  group.add(doorGroup);
  return doorGroup;
}

Click on the door after the modification, and you will find that the door opens around the expected rotation axis. As shown below

4. Create a skybox

The skybox here is very simple. Use the built-in SphereGeometrygeometry to create a circle with the same radius as the ground, and then load the texture

//天空盒
function createSkyBox() {
  const texture = new THREE.TextureLoader().load("/img/sky.jpg");
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
  // texture.repeat.set(1, 1);
  const skyBox = new THREE.SphereGeometry(500, 100, 100);
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    side: THREE.BackSide,
  });
  const skyBoxMesh = new THREE.Mesh(skyBox, material);
  scene.add(skyBoxMesh);
}

The final effect is as follows

 At the end you create a few more houses by looping, like this

 Or view the document and switch to the first-person controller to navigate in the scene created by yourself.

This article is reproduced in:

https://juejin.cn/post/7200571354926858301

If it is helpful to you, you are welcome to pay attention, I will update the technical documents regularly, let us discuss and learn together, and make progress together.

 

Guess you like

Origin blog.csdn.net/qq_40716795/article/details/129723034