第169期:谷仓(一)熟悉Three.Js基本操作

434 阅读7分钟

封面图

近期准备学习一下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中的一些基本概念及方法属性,下一篇文章将会在此基础上添加一些稍微复杂的交互,我们一起学习一下吧。