大学时一直很喜欢玩minecraft这一款游戏,毕业之后便没有太多的时间玩游戏,但是对其的关注确一直没有减少,当网易获得我的世界代理权时,看到网易官网做的炫酷的3Dbanner。非常炫酷,于是萌生了做一个简单的网页版的世界的想法。
一,准备工作
1)库/框架选型
THREE.JS
要了解THREE.JS,首先要了解一下什么是WEBGL,WebGL(全写Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化.
ES6
这个不用多说
2)构建工具
WebPack
WebPack是一个模块打包工具,它可以分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。
二,项目构建
Webpack4 + ES6
环境搭建,可以看我之前的一篇博文,这里不再赘述。Webpack + ES6 最新环境搭建与配置
三,主要功能实现
对于THREE.JS的一些基本知识本章不重点叙述,园子里很多博文写得非常很清楚,大家自行查找。本文只叙述实现本节相关的代码。以下连接是官方提供的一个简单的在3D空间中生成立方体的例子,简单的叙述了如何使用three.js的基本用法和怎么产生方块。读懂它对本例的理解会有非常大的帮助。
https://threejs.org/examples
1 )构建地面
构建地面我们要做什么
1 > 在3D空间中以(0.0.0)为基准产生一串连续的数组作为地形坐标
地面生成算法,这里引用了 Improved Noise reference implementation ,three官方例子 webgl_geometry_minecraft,参考THREE.JS的例子,本例做出了一些调整。由于作者这方面的基础不是很好,不多做叙述。
2 > 在每个点的位置填充方块
细节1:只渲染可见的面
例如上图A方块,他的前后左右都有方块,那他的前后左右的面都可以不用渲染,B方块它的后面和右面都有方块,它的后面和右面就不用渲染。只渲染需要的面,能够缩小渲染时间,提高帧率。
细节2:为暗面添加阴影
如果一个方块四周的方块都比他低,那它是最亮的。如果他的四周的方块都高于他,那他就会暗一些。如上图的绿色区域,绿色区域就会相对较暗。下面是效果对比。
2)玩家视角
我们所看到的画面是摄像机拍摄的画面,我们将摄像机当作玩家的眼睛,玩家使用键盘产生的动作来驱动摄像机移动,即可产生第一人称的效果。
相关介绍:
Pointer Lock API
它可以访问原始的鼠标运动,把鼠标事件的目标锁定到一个单独的元素,这就消除了鼠标在一个单独的方向上到底可以移动多远这方面的限制,并从视图中删去光标。这样就可以让我们通过移动鼠标而不需要点击任何按钮就可以控制视角。
THREE.PointerLockControls
THREE.JS提供了PointerLockControls控制器,实例化之后可以通过控制器对象的getObject()获取到控制对象,设置控制对象的位置,在使用其提供的update函数实现人物的视角旋转。
1 > 视角移动
由Pointer Lock API与THREE.PointerLockControls完成,使用方式请参考本例代码或者 https://developer.mozilla.org/zh-CN/docs/API/Pointer_Lock_API。
2 > 人物移动与物体碰撞
通过监听键盘的按键和控制相机的位置来控制人物的移动。
找到物体的大致思路如1下图
鼠标在屏幕上点击的时候,得到二维坐标p(x, y),再加上深度坐标的范围(0, 1), 就可以形成两个三位坐标A(x1, y1, 0), B(x2, y, 1), 由于它们的Z轴坐标是0和1,则转变到投影坐标系的话,一定分别是前剪切平面上的点和后剪切平面上的点,也就是说,在投影坐标系中,A点一定在能看见的所有模型的最前面,B点一定在能看见的所有的模型的最后边,将AB点连成线,AB线穿过的物体就是被点击的物体。而 Three.js提供一个射线类Raycasting来拾取场景里面的物体。更方便的使用鼠标来操作3D场景。(在本例我们组成射线的两个点是摄像机所在视点的相对位置与人物移动方向形成的射线)
详细的官方文档: Raycaster
new Raycaster( origin, direction, near, far ); origin — 光线投射的起点向量。 direction — 光线投射的方向向量。 near — 投射近点,用来限定返回比near要远的结果。near不能为负数。缺省为0。 far — 投射远点,用来限定返回比far要近的结果。far不能比near要小。缺省为无穷大。 var ray = new THREE.Raycaster(newTHREE.Vector3(),newTHREE.Vector3().copy(this.directionY),0,51); origin 初始化为(0,0,0),移动时在刷新 direction 初始化设置为Y方向,防止无限下坠, near 设置为0 far 设置为51 (半个方块 + 1)
通过获取到控制器对象的位置(相机的位置),将视线位置的Y值-200,就可以获取到史蒂夫脚方块的位置,如果脚的位置到地面的位置小于等于50(方块宽度的一半),则说明史蒂夫的脚碰到的地面,就停止相机的下坠。对于碰撞也是相同的做法,如果脚方块与移动方向前50PX内无方块,则可以移动,否则停止。移动的方向我们通过摄像头的方向(史蒂夫在三维空间视觉方向的矢量)和按键来确定,如果往前移动,那么检测的方向就是视觉方向,如果往左移动,则需要将矢量逆时针旋转90度后作为检测的方向。
控制器核心代码如下:
update() { //判断鼠标是否锁定 if (this.controlsEnabled === true) { //判断键盘移动方向 this.direction.z = Number(this.moveForward) - Number(this.moveBackward); this.direction.x = Number(this.moveLeft) - Number(this.moveRight); this.direction.normalize(); // this ensures consistent movements in all directions //用于检测地面 this.raycaster.ray.origin.copy(this.controls.getObject().position); //相机距离地面的相对高度 this.raycaster.ray.origin.y -= this.height; let intersections = this.raycaster.intersectObjects([this.load]); //将摄像头面对的相对方向置入cameradir变量,用于检测障碍物. this.controls.getObject().getWorldDirection(this.cameradir); let intersections2 = [], onWard = false, dir = new THREE.Vector3(); //确认移动的方向,以便检测障碍物 if (this.direction.z > 0) { //W dir.copy(this.cameradir).negate(); } else if (this.direction.z < 0) {// //S dir.copy(this.cameradir); } if (this.direction.x > 0) { //A dir.copy(this.cameradir).applyAxisAngle(this.directionY, 90.0); } else if (this.direction.x < 0) { //D dir.copy(this.cameradir).applyAxisAngle(this.directionY, -90.0); } //用于检测障碍物 if (this.direction.z != 0 || this.direction.x != 0) { this.raycaster.ray.direction.copy(dir); intersections2 = this.raycaster.intersectObjects([this.load]); this.raycaster.ray.direction.copy(this.directionY); } //是否碰撞标志位 onWard = intersections2.length > 0; //是否下坠标识位 let onObject = intersections.length > 0; //一帧的时间 大概0.016S let time = performance.now(); let delta = (time - this.prevTime) / 1000; //delta = 0.016; 调试时使用此项 //以下为了保证有过渡效果 this.velocity.x -= this.velocity.x * 10 * delta; this.velocity.z -= this.velocity.z * 10 * delta; this.velocity.y -= 9.8 * this.downSpeed * delta; // 100.0 = mass //人物移动距离计算 if (this.moveForward || this.moveBackward) { this.velocity.z -= this.direction.z * this.moveSpeed * delta; } if (this.moveLeft || this.moveRight) { this.velocity.x -= this.direction.x * this.moveSpeed * delta; } //确保人物不下落 if (onObject === true) { this.velocity.y = Math.max(0, this.velocity.y); this.canJump = true; } //确保人物碰撞停止 if (onWard === true) { if ((this.direction.x != 0)) { this.velocity.x = 0; } if ((this.direction.z != 0)) { this.velocity.z = 0; } } //人物移动 this.controls.getObject().translateX(this.velocity.x * delta); this.controls.getObject().translateY(this.velocity.y * delta); this.controls.getObject().translateZ(this.velocity.z * delta); this.prevTime = time; } }
代码下载地址:https://github.com/sincw/sinwProject/tree/master/MineCraftWeb
这样就能够完成一个基础的我的世界了,请期待后续。