vlambda博客
学习文章列表

学习Threejs的那些事儿

Threejs---Web端的三维可视化引擎库

  • 写在最开始的话

  • 从首页例子说起

    • 1 搭建threejs环境


写在最开始的话

    (关心技术内容的小伙伴这一段就跳过吧,是我自己瞎扯了)终于终于,我开始了threejs的学习。回忆过往,这是我自毕业后再次重新开始学习三维可视化方面的相关内容,并且是依托于web进行学习。毕业已将近一年,也近一年没有再碰过三维方面的的内容,心中一直对其十分惦念。由于本人在学校里从事的是计算机视觉中的三维重建方向,因此对于影像,点云等方面的内容进行了深入研究,编程语言学习的是C++以及Winform,.Net那一套C/S端的开发模式。然而毕业后因各种原因,从事了web端的0基础学习与开发。现在自己web端的技术水平不能说达到大牛,但是对web端的开发流程与模式都有了较为深入的了解,能够自己独立的进行web应用的开发。但我心中对于三维开发一直充满着热情。因此,今天在CSDN上开始记录自己threejs学习的点滴路程,一是为了记录自己在学习过程中所遇到的坑,为同样对三维感兴趣的小伙伴提供些参考,更重要的是是为了督促自己学习,为自己提供学习的动力(你们对我的关注就是我最大的动力啦~~,我想看着自己的博客阅读量一天天增加,心里肯定会特别高兴),避免自己懒惰而拖更,所以我就暂定每周更新一篇吧。
    在本专栏中,我将记录自己的学习的过程,学习资料主要是从根据未雨绸缪,暮志未晚进行,这里面涵盖了大量的threejs案例,我准备挑选自己喜欢的案例进行学习并总结其中遇到的问题,记录自己感兴趣的部分。同时本人在公司从事了较多的vue前端开发,因此我还会记录vue+threejs的开发过程。

从首页例子说起

1 搭建threejs环境

  • three.js 该文件主要是应用在一般的html文件中,使用< script >标签进行引入。经过实现发现,使用本地形式的引入,在旋转场景时更快;但在vue项目中使用后,旋转场景时明显感觉到不如前者顺畅。

  • three.min.js 打开该文件,与three.js进行对比,可以发现前者更像后者的压缩版,可能正因如此,才命名为“min”吧。回想其他js库,也都可以发现这样的命名方式。所以在html中引入时,使用这两者是都可以的。

  • three.module.js 这个文件顾名思义是模块化的库文件,适用于es6的语法。从例子中你可以看到这个文件的使用方法。

可气的是,后来找到了threejs中文网,上面对源码master的目录结构有非常详细的描述,这就是不好好看说明文档的下场,切记~~顺便把官网上的介绍图放在这里。仔细读下来,就发现学习threejs只要好好研究master源码和其中的例子就完全ok了。
学习Threejs的那些事儿
    接下来进入正题,讲述一下我复现上述网站的背景案例。其实代码作者已经提供了,我所做的主要工作有两个方面。其一为使用html,直接复现案例;其二是在vue项目中复现案例。
学习Threejs的那些事儿
    对于如何在html中复现,相信大家都会觉得很简单。我的文件目录如下:
学习Threejs的那些事儿
大家可以看到,我把编译好的three.js文件直接拿了过来,还有使用npm命令下载的threejs库中的OrbitControls.js和TrackballControls.js,这两个文件主要是实现鼠标控制场景的,具体不同之处我还没有深入研究。其他的文件比如dat库,其他教程上可能还有stats库,都是可有可无,并不影响threejs的关键功能。然后把例子中的代码拷贝进去,就ok了。
    对于如何在vue项目中复现案例,由于我查阅了其他教程,描述的并不详细,因为我在复现过程中就遇到了许多问题,因此我着重讲解我在复现过程中遇到的问题。首先,在前端框架上,为了方便,我直接使用了vue-element-admin,直接添加一个三维模块的路由,然后在添加自己的三维模块组件就ok了。效果如图所示
学习Threejs的那些事儿
在vue中使用threejs,第一步当然是使用npm install threejs进行依赖库安装,这一点本文和其他教程大同小异。本文这里并没有全局引入threejs,只是在自己新添加的ThreeDiimension组件中引入了,全局引入的方法和其他库全局引入的方法相同。先为大家呈上我的ThreeDiimension组件代码:

<template>
<div class="app-container">
<div id="secen-container" ref="secenContainer" />
</div>
</template>

<script>
import * as THREE from 'three'
import * as dat from 'dat.gui'
// import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// const OrbitControls = require('three-orbit-controls')(THREE)
export default {
name: 'ThreeDimension',
data() {
return {
container: null,
render: null,
camera: null,
scene: null,
raycaster: new THREE.Raycaster(),
mouse: new THREE.Vector2(),
dat: null,
stats: null,
controls: null
}
},
created() {
},
mounted() {
this.initRender()
this.initScene()
this.initCamera()
this.initLight()
this.initModel()
// this.initGui()
this.initControls()
this.initStats()

this.animate()
window.onresize = this.onWindowResize
window.addEventListener('click', this.onMouseClick, false)
},
methods: {
initRender: function() {
this.render = new THREE.WebGLRenderer({ antialias: true, alpha: true })
this.render.setSize(this.$refs.secenContainer.offsetWidth, this.$refs.secenContainer.offsetHeight)

this.render.shadowMap.enabled = true
this.render.shadowMap.type = THREE.PCFSoftShadowMap
this.render.setClearColor(0xffffff)

this.container = document.getElementById('secen-container')
this.container.appendChild(this.render.domElement)
},
initCamera: function() {
this.camera = new THREE.PerspectiveCamera(75, this.$refs.secenContainer.offsetWidth / this.$refs.secenContainer.offsetHeight, 0.1, 1000)
this.camera.position.set(0, 40, 100)
this.camera.lookAt(new THREE.Vector3(0, 0, 0))
},
initScene: function() {
this.scene = new THREE.Scene()
},
initLight: function() {

},
initModel: function() {
var helper = new THREE.AxesHelper(10)
this.scene.add(helper)

var s = 25

var cube = new THREE.CubeGeometry(s, s, s)

for (var i = 0; i < 3000; i++) {
var material = new THREE.MeshBasicMaterial({ color: this.randomColor() })

var mesh = new THREE.Mesh(cube, material)

mesh.position.x = 800 * (2.0 * Math.random() - 1.0)
mesh.position.y = 800 * (2.0 * Math.random() - 1.0)
mesh.position.z = 800 * (2.0 * Math.random() - 1.0)

mesh.rotation.x = Math.random() * Math.PI
mesh.rotation.y = Math.random() * Math.PI
mesh.rotation.z = Math.random() * Math.PI

mesh.updateMatrix()

this.scene.add(mesh)
}
},
initGui: function() {
this.gui = new dat.GUI()
},
initStats: function() {
/* this.stats = new Stats()
this.container.appendChild(this.stats) */

},
initControls: function() {
this.controls = new TrackballControls(this.camera, this.render.domElement)
this.controls.enableDamping = true
this.controls.enableZoom = true
this.controls.autoRotate = false
this.controls.minDistance = 50
this.controls.maxDistance = 200
this.controls.enablePan = true
},
animate: function() {
this.threeRender()
// this.stats.update()
this.controls.update()
requestAnimationFrame(this.animate)
},
randomColor: function() {
var arrHex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
var strHex = '#'
var index = ''
for (var i = 0; i < 6; i++) {
index = Math.round(Math.random() * 15)
strHex += arrHex[index]
}
return strHex
},
onMouseClick: function(event) {
this.mouse.x = ((event.clientX - 210) / this.$refs.secenContainer.offsetWidth) * 2 - 1
this.mouse.y = -((event.clientY - 84) / this.$refs.secenContainer.offsetHeight) * 2 + 1

// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
this.raycaster.setFromCamera(this.mouse, this.camera)

// 获取raycaster直线和所有模型相交的数组集合
var intersects = this.raycaster.intersectObjects(this.scene.children)

// 将所有的相交的模型的颜色设置为红色,如果只需要将第一个触发事件,那就数组的第一个模型改变颜色即可
if (intersects.length > 0) {
intersects[0].object.material.color.set(0xff0000)
}
},
onWindowResize: function() {
this.camera.aspect = this.$refs.secenContainer.offsetWidth / this.$refs.secenContainer.offsetHeight
this.camera.updateProjectionMatrix()
this.threeRender()
this.render.setSize(this.$refs.secenContainer.offsetWidth, this.$refs.secenContainer.offsetHeight)
},
threeRender: function() {
this.render.render(this.scene, this.camera)
}
}
}
</script>

<style lang="scss" scoped>
.app-container{
width: 100%;
min-height: 100%;
position: absolute;
padding: unset;
#secen-container{
width: 100%;
min-height: 100%;
position: absolute;
}
}
</style>

这个例子实现了threejs中的基本功能,包括初始化场景,相机,添加模型,以及我最感兴趣的鼠标控制场景功能。在vue中,我把所有用到的场景元素放在了data()中,例如render,camera,scene等。大多数教程中都是把canvas的容器铺满整个屏幕,但通过我上面放的效果图,大家可以看到我并没有把容器铺满全屏,这个形式带来的最大的影响就是选取场景中模型不能使用案例中原始的代码。

onMouseClick: function(event) {
this.mouse.x = ((event.clientX - 210) / this.$refs.secenContainer.offsetWidth) * 2 - 1
this.mouse.y = -((event.clientY - 84) / this.$refs.secenContainer.offsetHeight) * 2 + 1

// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
this.raycaster.setFromCamera(this.mouse, this.camera)

// 获取raycaster直线和所有模型相交的数组集合
var intersects = this.raycaster.intersectObjects(this.scene.children)

// 将所有的相交的模型的颜色设置为红色,如果只需要将第一个触发事件,那就数组的第一个模型改变颜色即可
if (intersects.length > 0) {
intersects[0].object.material.color.set(0xff0000)
}
},

最主要的问题就是出在event.clientX 与event.clientY ,我在这里分别减去了210与84个像素,分别是我左侧边栏的宽度与容器上方导航栏与面包屑的高度。我本来是想动态获取这两个值。学习过vue-element-admin的同学应该知道这个问题就是获取兄弟组件的宽高,但是我不想利用vuex获取,所以暂时还没找到方法。有好方法的同学让我学习学习,不胜感激。现在特别不喜欢用vuex,因为这个东西每次F5刷新页面后,store里的变量都会不存在了,所以这个“全局变量”不好用。当然也有可能是我还没有学到精髓,哈哈~~
另外,在选用OrbitControls和TrackballControls时,我还遇到了不少小坑。其实这两个控制器都可以控制场景旋转等功能。我在查阅资料时,看到许多博客都说使用three-orbit-controls或者three-orbitcontrols这两个库。的确,我尝试过后是可以使用无误的,但是有强迫症的我并不想引入那么多的库,所以查看node_modules里threejs的源码,发现有js和jsm两个文件夹,而且里面都有OrbitControls.js和TrackballControls.js,所以我分别进行了尝试。使用js里的控制器会出现问题,但jsm里的控制器是可以的,应该是由于我的项目里使用es6模块化的语法吧,所以才要采用threejs的模块化库文件。当使用js文件夹里的时候,会报如下错误

所以我在OrbitControls.js的最上方加入了‘import * as THREE from ‘three’’,但又出现了下面的错误,

所以,js文件夹里的控制器在vue里就用不了,换成jsm里的控制器问题就解决了!
这篇文章就到这里了,期待大家的回复哦!