我把两个房子放进电脑里了——three.js初体验

3,601 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

作为一个练习了两年半的前端,已经厌倦了日复一日的还原那些臭虾ui给出的毫无美感可言的页面,每当看到他们那千篇一律复制黏贴的静态图,都有一种恶心的冲动。于是摸鱼成了常态,想起了曾经有过一面之缘的three.js,于是研究了几天。感觉异常舒适,让我这枯燥的生活多了几分乐趣,于是作为一个半路出家且毫无3D经验的前端练习生,我又兴奋的练了起来。

一、基本概念

一般来说,我们用到three.js开发的场景都是3D的,跟我们平时的前端开发截然不同,基本上可以说是两个领域的东西,很多人一开始看都会被这些复杂的概念给搞头晕,我当时也是。因此一些基本的概念我会以我学的时候好理解的方式来呈现。

在three.js中,一个3D场景的构建由以下几个大的部分组成:

  1. 摄像机
  2. 场景
  3. 渲染器

其中,摄像机跟我们平时用手机机拍照一个意思,可以看作是一个取景器,我们只能看到摄像机中所呈现的内容,视野范围之外的内容、屏幕之外的内容我们无法看见。我们可以通过移动摄像机的位置来模拟人转头、前进后退的视觉效果。

场景基本上等同于现实世界中的一个小景观,是一个容器,里面包含了很多东西,比如网格、材质、几何体、灯光等等。摄像机中取景显示的内容就是我们的场景

渲染器也可以看作是一个容器,假设以上的所有东西我们都需要购买,也已经购买好了送到手里了,那么我们就要找一个地方放它。这个放它的地方就是渲染器在的地方,我们需要把我们的摄像机和场景放到渲染器中,在three.js中,当创建渲染器后如果没有指定渲染到哪一块区域,它会帮我们自动创建一块区域并且添加到页面中,如果指定了,那么场景会渲染到我们指定的容器中。

二、关于以上三大件

下图是three.js中的代码组织结构

image.png 通过上图我们可以看到,在三大件下方还藏着许多小东西。老话说好记性不如烂笔头,所以我直接上代码,我们会构造一个简单的环境。

在本篇中我们会构建一个如下图所示的场景。

image.png

image.png

为啥我一来就要搞一个这个看上去很复杂的场景呢。那是因为我学的时候全是一些立方体啥的我觉得太无聊了,太枯燥,不得劲。虽然看着复杂,但是实际上并不复杂。废话不多说,我们开始。

环境准备

这里我用的是vite+vue3的基础环境,在此之上我们还需要安装three.js

1 在目录下执行 npm i three

2 安装完后将以下我们用上的文件夹提取到你的项目目录下,将loadders,controls文件夹提取到目录下,如下图所示

image.png

接着创建基本的页面结构,这里我为了方便省事,我就直接写在App.vue中了

image.png

<template>
  <canvas id="draw"></canvas>
</template>

这是采用的是setup语法糖,根据个人喜好基本都大同小异

首先让我们导入three主体

image.png

import * as THREE from "three";

引入vue3的onMounted钩子,后续我们的代码会写在回调中 image.png

import { onMounted } from "vue";

三、开始创建

准备工作完成了,接着让我们来着手创建一个场景吧,上面我们在页面中创建了一个id名为draw的canvas,在这里我们可以不用设置它的大小,可以在后续的渲染器当中设置。

获取canvas
  const canvas = document.querySelector("#draw");
创建场景容器

接着,我们应该做什么呢,画布准备好了,接下来我们要创建一个场景,我觉得更准确的来说是一个容器。

const scene = new THREE.Scene();
创建并设置相机

好了,我们的场景容器创建好了,可以看到现在我们的页面中啥都没有,让我们接着干。 下面我们创建一个相机,相机也相当于人的眼睛,我们眼睛是有视野范围的。

//获取屏幕宽高
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(100, width / height, 0.1, 1500);

上面这行代码我们创建了一个透视相机,这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式。说通俗点就是会产生近大远小的效果。 PerspectiveCamera继承于Camera,有四个参数,上面这行代码中的100代表视野范围fov,玩过端游的吃鸡吗,设置选项中的视野范围也就是这个,越大代表呈现在相机中的东西就越多。 width / height 代表长宽比,0.1代表近端面的距离near1500代表远端面的距离far,这些参数一起定义了摄像机的视锥体,如下图所示。其中棱台的部分为摄像机能看到范围

image.png 以上代码我们创建了一个相机并且设置了相机了各个参数。我们可以看到画面中还是啥都没有。 接下来让我们把相机添加到场景中

  scene.add(camera);

以上这行代码就将相机添加到我们创建的场景中了,当我们调用场景的add方法时,物体会被默认添加到场景的(0,0,0)处,看到这里你是否感觉到不对劲了,对的,还没有介绍three.js中的坐标系。因此,下面我们说一下坐标。

image.png three.js中的坐标如上图所示。很好理解是不是,这里就不多说了。 因此,如果我们的相机位置在原点的话,不是很方便,所以我们设置一下相机的位置。将上面的代码变成下面这样

  camera.position.set(8, 1, 12);
  scene.add(camera);

我们的第一个相机就完成了,下面我们创建地板

创建地板(草地)

让我们先来创建一个平面缓冲几何体 PlaneGeometry

  const floor = new THREE.PlaneGeometry(1500, 1500);

这个构造函数有四个参数,这里用了长和宽,另外两个后面再说。 然后为地板加载纹理。啥是纹理,你可以理解成你英雄联盟英雄的皮肤,可以让他们变好看。

  const floorTexture = new THREE.TextureLoader().load("/cao.webp");

我这些图片都是网上随便找的,所以你们随便找都行。然后设置纹理

floorTexture.repeat.set(1500, 1500);

下面是设置重复次数,因为我的这个图显然没有地板大,所以我就重复1500次,实际上不重复也行。 接下来我们要创建材质并且应用上面创建的纹理

  const floorMaterial = new THREE.MeshBasicMaterial({ map: floorTexture });

MeshBasicMaterial是一个基础网格材质,不受后面会说的光照的影响,说白了就是不会反光。 接下来创建一个网格,把上面创建的floor和floorMaterialr传入。我目前的理解网格就是场景中组合、分类物体的东西

  const floorMesh = new THREE.Mesh(floor, floorMaterial);

将地板添加到场景中

  scene.add(floorMesh);

然后我们一看,卧槽,咋还是啥都没有。是不是哪里写错了,当然不是,前面说了有三大件,你看我们现在才几个,一个场景一个摄像机,才两个对不对,我们一直都没有添加渲染器。那么,为了让我们的地板能被看到,我们接下来创建渲染器

  const renderer = new THREE.WebGLRenderer({
    canvas,//渲染器绘制其输出的canvas
    antialias: true,//是否抗锯齿
    alpha: true,//canvas是否包含alpha (透明度)。默认为false
  });
  //设置渲染器大小,这里设置为整个窗口,会自动设置canvas的大小
  renderer.setSize(width, height);
  //调用渲染器的渲染方法,告诉它要渲染了
 // renderer.render(scene, camera);
  render()//调用渲染方法

为了后续操作,写了一个方法

  function render() {
    renderer.render(scene, camera);
  }

渲染器有了,咋还是啥都没有,现在我们的场景可以看作是一个小黑屋,伸手不见五指的那种,那么要能看见我们要干嘛呢,当然是开灯了,上面的图不知道还记得嘛,我在再放一遍

image.png 网格模型我们有了,还没有光照,下面我们创建光照,为了方便,我选择创建环境光,环境光会均匀的照亮场景中的所有物体。

//三个参数分别为光照颜色 、 光照强度
  const light = new THREE.AmbientLight(0x404040, 3);
  scene.add(light);

好了灯光有了,让我们看看。

页面中的地板出来了,一看地板这地板怎么是竖的,不对啊。怎么是这样的呢

image.png 原来这里是因为我们创建地板的时候传入的宽高对应的是x、y,根据前面说的坐标系来说,出来的那可不是竖的嘛,因此,让我们旋转一下地板,在将地板网格添加到场景前增加一行代码,设置绕x轴旋转,设置完后可以看到地板横过来了,更像是一个地板了

  floorMesh.rotation.x = Math.PI * -0.5;
  scene.add(floorMesh);

image.png

加载模型

接下来就是简单的活了,我们加载模型,我的模型是在windows的3d资源查看器中找的。点击资源库,找一个你喜欢的模型

image.png 加载完成后点击另存为会保存为glb格式,这里我所有的资源都是放在了public目录下

image.png 接下来正式开始加载模型 要加载模型我们需要引入正确的加载器 前面的准备工作中已经把loader文件夹放到src目录下了

引入加载器

import { GLTFLoader } from "@/loaders/GLTFLoader";

这里写了一个加载方法

  const loader = new GLTFLoader();
  function loadModel(path) {
    return new Promise((resolve, reject) => {
      const loader = new GLTFLoader();
      loader.load(path, resolve);
    });

加载房

  let houseModel1, houseModel2
  let house1 = loadModel("Home.glb").then((res) => (houseModel1 = res));
  let house2 = loadModel("Residential House.glb").then(
    (res) => (houseModel2 = res)
  );
  Promise.all([house1, house2, flower]).then((val) => {
    //房子1
    houseModel1.scene.position.set(0, 0, 0);//设置模型位置
    houseModel1.scene.scale.set(20, 20, 20);//设置模型缩放
    scene.add(houseModel1.scene);
    //房子2
    houseModel2.scene.position.set(-25, 0, 10);
    houseModel2.scene.scale.set(7, 7, 7);
    houseModel2.scene.rotation.y = 90;
    scene.add(houseModel2.scene);

    render();
  });

加载树,树是随机坐标的,所以写了一个方法

   //num 数量 range范围
  function radomPoint(num, range) {
    let arr = [];
    for (let a = 0; a < num; a++) {
      arr.push([
        Math.random() * range - range / 2 - a,
        0,
        Math.random() * range - range / 2 - a,
      ]);
    }
    return arr;
  }
  // 树模型
  loader.load("Evergreen Tree.glb", (res) => {
    console.log(res.scene);
    let point = radomPoint(10, 100);
    console.log(point);
    for (let a = 0; a < point.length; a++) {
       //克隆模型
      let newModel = res.scene.clone();
      let scalNum = Math.random() * 20;
      newModel.scale.set(scalNum, scalNum, scalNum);
      newModel.position.set(...point[a]);
      scene.add(newModel);
    }
  });

然后看一下页面,大概就是下面这个样子了

image.png 这个时候会发现不能移动和放大缩小。下面我们增加控制器,这些three.js都给我们提供好了。 引入轨道控制器OrbitControls

import { OrbitControls } from "@/controls/OrbitControls";

设置控制对象

//camera :要被控制的相机。该相机不允许是其他任何对象的子级,除非该对象是场景自身。
//canvas :domElement用于事件监听的HTML元素。
  const controls = new OrbitControls(camera, canvas);

控制器创建完了,会发现我们的场景还是不能动 更改render方法为下面这样

  function render() {
    controls.update();//控制器更新
    requestAnimationFrame(render);
    renderer.render(scene, camera);
  }

现在我们就可以拖动、调整摄像机角度了。