封面图
近期准备学习一下THREE.JS相关的内容,所以网上找了些项目自己动手写一写,搞清楚相关的一些基本知识和原理。
这篇文章主要记录一下如何实现封面图中所展示的内容。
场景 & 相机 & 灯光 & 渲染器
场景(scene)、相机(camera)、灯光(light)、渲染器(renderer)是three.js中的四个基本概念。场景好比是现实中的舞台、相机好比是我们的眼睛、灯光则可以理解为舞台上的各种灯光,比如:环境光、自然光、平行光、射灯灯光等。
演员和道具被我们添加到场景中,我们通过控制相机以及灯光的位置移动来实现各种不同的效果,这就是使用three.js进行开发的基本原理。
初始化项目
这个项目采用vue3+ts进行复制,使用vite直接创建的项目,命令如下:
npm create vite@latest
然后进入我们指定的文件夹启动项目
cd 3d-gucang
npm install
npm run dev
导入项目中需要的模型以及贴图资源后就可以开始进行开发了。
分析
根据图片进行分析,项目中用到的模型有 围墙、高的圆柱筒、矮的圆柱筒、矮房子。用到的贴图有:地面、围栏、圆柱体的贴图、以及矮房子的贴图,并且这些内容肉眼可见的分为三个区域。
所以在开发过程中,我们能够了解到的内容大致有:
- 如何加载模型
- 如何创建贴图
- 如何使用分组
开始
首先,我们需要创建最基本的条件,创建场景,相机,渲染器。
import * as THREE from 'three';
// 创建场景
let scene = new THREE.Scene()
// 创建相机
let width = window.innerWidth;
let height = window.innerHeight;
let k = width / height;
let s = 100;
let camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 20000);
camera.position.set(314, 202, 243);
camera.lookAt(scene.position);
// 创建渲染器
let renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth,window.innerHeight)
renderer.render(scene,camera)
// 将渲染器添加到页面中
document.body.appendChild(renderer.domElement)
此时,我们刷新页面,发现页面上什么都没有,如图:
我们添加一个测试用的球体:
// 测试物体
const geom = new THREE.SphereGeometry(12, 32, 12)
const mate = new THREE.MeshBasicMaterial({
color: 0x00ff00
})
const box = new THREE.Mesh(geom, mate)
scene.add(box)
SphereGeometry表示球形几何体,它的参数为:半径、水平分段数、垂直分段数。
MeshBasicMaterial表示绘制物体所以使用的是基础材质,我们给它设置了一个颜色:color: 0x00ff00.
Mesh表示基于三角形多边形网格的物体对象。它的构造函数接受两个参数:geometry和material,用来创建一个物体对象。
此时我们刷新页面,可以在页面中看到一个球体。
表明目前为止,我们的程序没有什么错误。
加载背景
我们的背景现在是黑色,我们可以给场景设置一个我们需要的背景,这需要使用TextureLoade。
let textureLoader = new THREE.TextureLoader();
let texture = textureLoader.load("src/assets/background.jpeg");
scene.background = texture;
此时我们发现背景变成了蓝色
加载围墙
围墙是一个obj格式的模型,所以需要我们使用OBJLoader加载器进行加载。
import * as THREE form 'three'
import {OBJLoader} from 'three/examples/jsm/loaders/OBJLoader';
// 加载围墙
const loadWall = () =>{
const objLoader = new OBJLoader()
objLoader.load('src/assets/wall.obj', function (obj) {
console.log('obj', obj)
obj.scale.set(0.98, 0.6, 1);
let textLan = new THREE.TextureLoader().load('src/assets/wall.png')
textLan.wrapS = THREE.RepeatWrapping
textLan.wrapT = THREE.RepeatWrapping
textLan.repeat.set(40, 1)
// 围墙
obj.children[0].material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
map: textLan,
transparent: true,
})
// 大门
obj.children[1].material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
map: new THREE.TextureLoader().load('src/assets/door.png'),
transparent: true,
})
scene.add(obj)
})
}
通过objLoader加载到围墙的模型后,在回调函数中对模型进行处理。围墙模型有两个子对象,一个是围墙,另一个是大门,我们通过TextureLoader()加载到对应的纹理贴图,并通过MeshBasicMaterial的map属性进行赋值,刷新页面,我们可以看到如下效果。
加载地面背景
地面的处理非常简单,我们只需要创建一个平面几何体,将它大小调试合适,位置调整合理,并给它贴上对应的纹理即可。
// 加载地面
const loadGround = () => {
const groundPlane = new THREE.PlaneGeometry(260, 260)
const groundTexture = new THREE.TextureLoader().load('src/assets/floor3.png')
const groundMate = new THREE.MeshBasicMaterial({
map: groundTexture,
// color: 0xffffff,
side: THREE.DoubleSide,
transparent: true,
})
const ground = new THREE.Mesh(groundPlane, groundMate)
ground.rotateX(-Math.PI / 2)
scene.add(ground)
}
通过THREE.PlaneGeometry()创建了一个平面几何体,并通过THREE.TextureLoader及THREE.MeshBasicMaterial的map属性,给地面贴上了它的纹理,此时刷新页面,效果如下:
加载谷仓
在封面图中我们可以看到谷仓有三种类型:高谷仓、矮谷仓、和平房仓。
所以这里需要用到一个分组的概念,将相同的物体轨迹到同一个组里进行处理。比如这里高谷仓是一个组,矮谷仓是一个组,平房仓是一个组。
加载高谷仓
渲染谷仓这部分需要对加载各自的模型,VS code中我们可以安装3D-viewer for vscode 这个插件来预览这里的模型,比如:
右上角可以查看该模型的一些相关信息。
在真正加载这个高谷仓之前,我们需要先创建一个分组,因为一共有6个高谷仓,并且这6个谷仓是一组,所以我们的代码如下:
import * as THREE from 'three';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
const loadHighBarn = () =>{
// 高粮仓
objLoader.load('src/assets/gong001.obj', function (obj) {
let mesh = obj.children[0]
mesh.rotateZ(Math.PI) // 转180
mesh.translateY(-36);
for (let i = 0; i < 2; i++) {
for (let k = 0; k < 3; k++) {
let Mesh = mesh.clone()
Mesh.material = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('src/assets/modal4.png'),
transparent: true,
side: THREE.DoubleSide,
clipIntersection: true,
})
// console.log('Mesh', Mesh)
Mesh.No = `L-${i + 1}*${k + 1}`
Mesh.name = `立筒仓 L-${i + 1}*${k + 1}`
Mesh.translateX(i * 26)
Mesh.translateZ(k * 21)
Mesh.rotateY(Math.PI / 6)
// 添加到对应的组
firstGroup.add(Mesh)
Mesh.stockHeight = 36
Mesh.riceHeight = (15 * Math.random() + 20).toFixed(1)
Mesh.goodesName = '红豆'
Mesh.tempreture = (36 * (Math.random() / 10 + 0.9)).toFixed(1)
Mesh.weight = Mesh.riceHeight * 200
Mesh.icon = 'src/assets/green_bean.png'
let geom = new THREE.CylinderGeometry(8 - 0.2, 8 - 0.2, Mesh.riceHeight, 25)
let mate = new THREE.MeshLambertMaterial({
color: 0xb63427
})
let cylinderMesh = new THREE.Mesh(geom, mate)
Mesh.add(cylinderMesh)
cylinderMesh.translateY(36 - Mesh.riceHeight / 2)
}
}
})
}
loadHighBarn()
这里我们通过OBJLoader加载器来加载了这个模型,并且在回调函数中对模型进行了处理。
具体的处理过程是便利一个两行三列的数据,通过mesh.clone()方法对这个高筒粮仓进行复制,并使用TextureLoader().load()方法加载了对应的纹理,然后通过firstGroup.add(Mesh)将它们添加到同一个组中去。
这样,我们的页面会变成这个样子:
同理,用同样的方法加载矮粮仓,和平房仓。
objLoader.load('src/assets/storage2.obj', function (obj) {
let mesh = obj.children[0]
mesh.rotateZ(Math.PI) // 转180
mesh.translateY(-20);
for (let i = 0; i < 2; i++) {
for (let k = 0; k < 6; k++) {
let Mesh = mesh.clone()
Mesh.material = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('src/assets/storage2.png'),
transparent: true,
side: THREE.DoubleSide,
clipIntersection: true,
})
// console.log('Mesh', Mesh)
Mesh.No = `Q-${i + 1}*${k + 1}`
Mesh.name = `浅圆仓 Q-${i + 1}*${k + 1}`
Mesh.translateX(i * 25)
Mesh.translateZ(k * 24)
Mesh.rotateY(Math.PI / 6)
sceondGroup.add(Mesh)
Mesh.stockHeight = 20
Mesh.riceHeight = (11 * Math.random() + 9).toFixed(1)
Mesh.goodesName = '绿豆'
Mesh.tempreture = (36 * (Math.random() / 10 + 0.9)).toFixed(1)
Mesh.weight = Mesh.riceHeight * 400
Mesh.icon = 'src/assets/green_bean.png'
let geom = new THREE.CylinderGeometry(10 - 0.2, 8 - 10.2, Mesh.riceHeight, 25)
let mate = new THREE.MeshLambertMaterial({
color: 0xb63427
})
let cylinderMesh = new THREE.Mesh(geom, mate)
Mesh.add(cylinderMesh)
cylinderMesh.translateY(20 - Mesh.riceHeight / 2)
}
}
})
// 平房仓
objLoader.load('src/assets/storage3.obj', function (obj) {
let mesh = obj.children[0]
mesh.translateZ(3);
mesh.translateX(1);
mesh.scale.set(1.3, 1.4, 1.5);
for (let i = 0; i < 2; i++) {
for (let k = 0; k < 3; k++) {
let Mesh = mesh.clone()
Mesh.material = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('src/assets/storage3.png'),
transparent: true,
side: THREE.DoubleSide,
clipIntersection: true,
})
// console.log('Mesh', Mesh)
Mesh.No = `P-${i + 1}*${k + 1}`
Mesh.name = `平房仓 P-${i + 1}*${k + 1}`
Mesh.translateX(i * 52)
Mesh.translateZ(k * 80)
// Mesh.rotateY(Math.PI / 6)
thirdGroup.add(Mesh)
Mesh.stockHeight = 8
Mesh.riceHeight = (4.9 * Math.random() + 3).toFixed(1)
Mesh.goodesName = '黄豆'
Mesh.tempreture = (36 * (Math.random() / 10 + 0.9)).toFixed(1)
Mesh.weight = Mesh.riceHeight * 1000
Mesh.icon = 'src/assets/yellow_bean.png'
let geom = new THREE.BoxGeometry(21 - 0.2, Mesh.riceHeight, 40 - 0.2, 25)
let mate = new THREE.MeshLambertMaterial({
color: 0xe99147
})
let cylinderMesh = new THREE.Mesh(geom, mate)
Mesh.add(cylinderMesh)
cylinderMesh.translateY(Mesh.riceHeight / 2)
}
}
})
刷新页面,就会出现下面的效果
如果我们的粮仓都是黑色,比如:
不要着急,这是因为我们没有将灯光添加进去,我们只需加点灯光效果即可。
// 灯光 没有灯光 物体的纹理默认不展示
var axesHelper = new THREE.AxesHelper(500);
scene.add(axesHelper)
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
directionalLight.position.set(50, 250, 500);
scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight2.position.set(-400, -400, -400);
scene.add(directionalLight2);
var ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
轨道控制
目前为止,我们可以渲染所有的模型以及相关的贴图,但是还不够炫酷,我们需要能够有点交互,拖拽一下让我们能看到各个角度的情况,如图:
这个也简单,我们只需要加入一下代码:
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
renderer.render(scene, camera); //执行渲染操作
});//监听鼠标、键盘事件
这时候我们刷新页面,就可以拖动进行查看了。
OrbitControls本质上就是改变相机的参数,比如相机的位置属性,改变相机位置也可以改变相机拍照场景中模型的角度,实现模型的360度旋转预览效果,改变透视投影相机距离模型的距离,就可以改变相机能看到的视野范围。
最后
通过对场景、相机、渲染器、灯光、物体&几何体、材质、贴图、模型加载器、分组以及轨道控制器的基本操作,我们简单熟悉了three.js中的一些基本概念及方法属性,下一篇文章将会在此基础上添加一些稍微复杂的交互,我们一起学习一下吧。