本文已参与「新人创作礼」活动,一起开启掘金创作之路。
作为一个练习了两年半的前端,已经厌倦了日复一日的还原那些臭虾ui给出的毫无美感可言的页面,每当看到他们那千篇一律复制黏贴的静态图,都有一种恶心的冲动。于是摸鱼成了常态,想起了曾经有过一面之缘的three.js,于是研究了几天。感觉异常舒适,让我这枯燥的生活多了几分乐趣,于是作为一个半路出家且毫无3D经验的前端练习生,我又兴奋的练了起来。
一、基本概念
一般来说,我们用到three.js开发的场景都是3D的,跟我们平时的前端开发截然不同,基本上可以说是两个领域的东西,很多人一开始看都会被这些复杂的概念给搞头晕,我当时也是。因此一些基本的概念我会以我学的时候好理解的方式来呈现。
在three.js中,一个3D场景的构建由以下几个大的部分组成:
- 摄像机
- 场景
- 渲染器
其中,摄像机跟我们平时用手机机拍照一个意思,可以看作是一个取景器,我们只能看到摄像机中所呈现的内容,视野范围之外的内容、屏幕之外的内容我们无法看见。我们可以通过移动摄像机的位置来模拟人转头、前进后退的视觉效果。
场景基本上等同于现实世界中的一个小景观,是一个容器,里面包含了很多东西,比如网格、材质、几何体、灯光等等。摄像机中取景显示的内容就是我们的场景
渲染器也可以看作是一个容器,假设以上的所有东西我们都需要购买,也已经购买好了送到手里了,那么我们就要找一个地方放它。这个放它的地方就是渲染器在的地方,我们需要把我们的摄像机和场景放到渲染器中,在three.js中,当创建渲染器后如果没有指定渲染到哪一块区域,它会帮我们自动创建一块区域并且添加到页面中,如果指定了,那么场景会渲染到我们指定的容器中。
二、关于以上三大件
下图是three.js中的代码组织结构
通过上图我们可以看到,在三大件下方还藏着许多小东西。老话说好记性不如烂笔头,所以我直接上代码,我们会构造一个简单的环境。
在本篇中我们会构建一个如下图所示的场景。
为啥我一来就要搞一个这个看上去很复杂的场景呢。那是因为我学的时候全是一些立方体啥的我觉得太无聊了,太枯燥,不得劲。虽然看着复杂,但是实际上并不复杂。废话不多说,我们开始。
环境准备
这里我用的是vite+vue3的基础环境,在此之上我们还需要安装three.js
1 在目录下执行 npm i three
2 安装完后将以下我们用上的文件夹提取到你的项目目录下,将loadders,controls文件夹提取到目录下,如下图所示
接着创建基本的页面结构,这里我为了方便省事,我就直接写在App.vue中了
<template>
<canvas id="draw"></canvas>
</template>
这是采用的是setup语法糖,根据个人喜好基本都大同小异
首先让我们导入three主体
import * as THREE from "three";
引入vue3的onMounted钩子,后续我们的代码会写在回调中
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
代表近端面的距离near
,1500
代表远端面的距离far
,这些参数一起定义了摄像机的视锥体,如下图所示。其中棱台的部分为摄像机能看到范围
以上代码我们创建了一个相机并且设置了相机了各个参数。我们可以看到画面中还是啥都没有。
接下来让我们把相机添加到场景中
scene.add(camera);
以上这行代码就将相机添加到我们创建的场景中了,当我们调用场景的add方法时,物体会被默认添加到场景的(0,0,0)
处,看到这里你是否感觉到不对劲了,对的,还没有介绍three.js中的坐标系。因此,下面我们说一下坐标。
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);
}
渲染器有了,咋还是啥都没有,现在我们的场景可以看作是一个小黑屋,伸手不见五指的那种,那么要能看见我们要干嘛呢,当然是开灯了,上面的图不知道还记得嘛,我在再放一遍
网格模型我们有了,还没有光照,下面我们创建光照,为了方便,我选择创建环境光,环境光会均匀的照亮场景中的所有物体。
//三个参数分别为光照颜色 、 光照强度
const light = new THREE.AmbientLight(0x404040, 3);
scene.add(light);
好了灯光有了,让我们看看。
页面中的地板出来了,一看地板这地板怎么是竖的,不对啊。怎么是这样的呢
原来这里是因为我们创建地板的时候传入的宽高对应的是x、y,根据前面说的坐标系来说,出来的那可不是竖的嘛,因此,让我们旋转一下地板,在将地板网格添加到场景前增加一行代码,设置绕x轴旋转,设置完后可以看到地板横过来了,更像是一个地板了
floorMesh.rotation.x = Math.PI * -0.5;
scene.add(floorMesh);
加载模型
接下来就是简单的活了,我们加载模型,我的模型是在windows的3d资源查看器中找的。点击资源库,找一个你喜欢的模型
加载完成后点击另存为会保存为glb格式,这里我所有的资源都是放在了public目录下
接下来正式开始加载模型
要加载模型我们需要引入正确的加载器
前面的准备工作中已经把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);
}
});
然后看一下页面,大概就是下面这个样子了
这个时候会发现不能移动和放大缩小。下面我们增加控制器,这些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);
}
现在我们就可以拖动、调整摄像机角度了。