简历
指针锁定API允许您在游戏界面中锁定鼠标或其他指针设备,以便您不用绝对定位光标就可以获得坐标变化值,从而准确地判断用户正在做什么,并且还可以防止用户意外地进入另一块屏幕或别的什么地方,从而导致误操作。
这一篇是我从官网上获得的相关控制器的,然后通过官网的案例进行一下修改扩展了一下功能。将案例实现了简单的碰撞检测。
案例实现
实现鼠标锁定
首先,我们在body里面添加了dom标签,因为鼠标锁定只有通过用户才可以触发。
<p id="blocker"> <p id="instructions"> <span style="font-size:40px">点击屏幕开始</span> <br /> <br /> (W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角) </p> </p>
然后,判断一下当前浏览器是否支持鼠标锁定。并绑定到了鼠标点击事件:
instructions.addEventListener( 'click', function ( event ) { instructions.style.display = 'none'; // 锁定鼠标光标 element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; element.requestPointerLock(); }, false );
实现鼠标改变视角
这里,就需要我们的PointerLockControls控件进行控制了,先引入PointerLockControls控件文件:
<script src="/lib/js/controls/PointerLockControls.js"></script>
实例化控件,并把相机传入:
controls = new THREE.PointerLockControls( camera );
通过实例化的对象可以通过getObject()获取到控制对象,可以设置它的位置来调整进入场景的位置。最后将对象放置到scene场景当中。
controls.getObject().position.y = 50; controls.getObject().position.x = 100; scene.add( controls.getObject() );
最后,在render当中,我们让controls调用update函数实现更新
//获取到控制器对象 var control = controls.getObject(); //获取刷新时间 var delta = clock.getDelta(); //根据速度值移动控制器 control.translateX( velocity.x * delta ); control.translateY( velocity.y * delta ); control.translateZ( velocity.z * delta );
实现按键移动
我们要实现可以通过键盘案例进行控制器位置调整,首先就是要监听事件,所以我们监听的键盘按下事件和键盘抬起事件:
document.addEventListener( 'keydown', onKeyDown, false ); document.addEventListener( 'keyup', onKeyUp, false );
然后再监听回调里面判断当前的键盘按键,符合条件的,就将当前的方向设置为true:
var onKeyDown = function ( event ) { switch ( event.keyCode ) { case 38: // up case 87: // w moveForward = true; break; case 37: // left case 65: // a moveLeft = true; break; case 40: // down case 83: // s moveBackward = true; break; case 39: // right case 68: // d moveRight = true; break; case 32: // space if ( canJump && spaceUp ) velocity.y += upSpeed; canJump = false; spaceUp = false; break; } };
最后,在render渲染中,进行判断当前的移动方向,实现的当前的移动,注意velocity向量,这是一个缓冲值,为了保证鼠标抬起后,场景不直接暂停,而是有一个简短的过渡效果:
if ( moveForward || moveBackward ) velocity.z -= direction.z * speed * delta; if ( moveLeft || moveRight ) velocity.x -= direction.x * speed * delta; //根据速度值移动控制器 control.translateX( velocity.x * delta ); control.translateY( velocity.y * delta ); control.translateZ( velocity.z * delta );
使用Raycaster实现简单的碰撞检测
这个部分是个难点,虽然以后会有更好的方法,但是,这个确实是一个好的练手方案。之前,我们使用Raycaster来进行窗口二维位置转three.js中的坐标位置。但是,Raycaster还有一个妙用,就是检测前方一定距离内是否有物体相撞。让我讲解一下实现原理。
- 首先,实例化Raycaster对象,然后,设置好Raycaster对象的,位置,方向,还有长度。Raycaster(起源:Vector3,方向:Vector3,近:浮动,远:浮动)
//声明射线 var upRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, 1, 0), 0, 10); var horizontalRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(), 0, 10); var downRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, -1, 0), 0, 10);
虽然声明了三个,但是,其实用到了只用了两个,第三个向上的判断也没有写。
然后,将当前控制器的位置赋值给射线
//复制相机的位置 downRaycaster.ray.origin.copy( control.position );
判断当前射线的线朝向的方向是否有物体
//判断是否停留在了立方体上面 var intersections = downRaycaster.intersectObjects( scene.children, true); var onObject = intersections.length > 0; //判断是否停在了立方体上面 if ( onObject === true ) { velocity.y = Math.max( 0, velocity.y ); canJump = true; }
这是y轴的判断,如果脚下有物体,则不再下坠。实现了简单的碰撞检测。
案例代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style type="text/css"> html, body { margin: 0; height: 100%; } canvas { display: block; } #blocker { position: absolute; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); } #instructions { width: 100%; height: 100%; display: -webkit-box; display: -moz-box; display: box; -webkit-box-orient: horizontal; -moz-box-orient: horizontal; box-orient: horizontal; -webkit-box-pack: center; -moz-box-pack: center; box-pack: center; -webkit-box-align: center; -moz-box-align: center; box-align: center; color: #ffffff; text-align: center; cursor: pointer; } </style> </head> <body onload="draw();"> <p id="blocker"> <p id="instructions"> <span style="font-size:40px">点击屏幕开始</span> <br /> <br /> (W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角) </p> </p> </body> <script src="/lib/three.js"></script> <script src="/lib/js/loaders/OBJLoader.js"></script> <script src="/lib/js/loaders/MTLLoader.js"></script> <script src="/lib/libs/chroma.js"></script> <!--处理颜色的库--> <script src="/lib/js/controls/PointerLockControls.js"></script> <script src="/lib/js/libs/stats.min.js"></script> <script src="/lib/js/libs/dat.gui.min.js"></script> <script src="/lib/js/Detector.js"></script> <script> var renderer,camera,scene,gui,light,stats,controls; var clock = new THREE.Clock(); //是否锁定页面的相关 var blocker = document.getElementById( 'blocker' ); var instructions = document.getElementById( 'instructions' ); //移动相关的变量 var controlsEnabled = false; var moveForward = false; var moveBackward = false; var moveLeft = false; var moveRight = false; var canJump = false; var spaceUp = true; //处理一直按着空格连续跳的问题 //声明射线 var upRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, 1, 0), 0, 10); var horizontalRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(), 0, 10); var downRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3( 0, -1, 0), 0, 10); var velocity = new THREE.Vector3(); //移动速度变量 var direction = new THREE.Vector3(); //移动的方向变量 var rotation = new THREE.Vector3(); //当前的相机朝向 var speed = 500; //控制器移动速度 var upSpeed = 200; //控制跳起时的速度 //辅助箭头 var up,horizontal,down,group; function initRender() { renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize(window.innerWidth, window.innerHeight); renderer.sortObjects = false; //告诉渲染器需要阴影效果 document.body.appendChild(renderer.domElement); } function initCamera() { camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 1000); //camera.position.set(0, 0, 50); } function initScene() { scene = new THREE.Scene(); } //初始化dat.GUI简化试验流程 function initGui() { //声明一个保存需求修改的相关数据的对象 //gui = {}; //var datGui = new dat.GUI(); //将设置属性添加到gui当中,gui.add(对象,属性,最小值,最大值) } function initLight() { scene.add(new THREE.AmbientLight(0x444444)); light = new THREE.PointLight(0xffffff); light.position.set(0,50,0); //告诉平行光需要开启阴影投射 light.castShadow = true; scene.add(light); } function initModel() { //辅助工具 var helper = new THREE.AxesHelper(50); //scene.add(helper); var mtlLoader = new THREE.MTLLoader(); mtlLoader.setPath('/lib/assets/models/'); //加载mtl文件 mtlLoader.load('city.mtl', function (material) { var objLoader = new THREE.OBJLoader(); //设置当前加载的纹理 objLoader.setMaterials(material); objLoader.setPath('/lib/assets/models/'); objLoader.load('city.obj', function (object) { //设置颜色的取值范围 var scale = chroma.scale(['yellow', '008ae5']); //重新设置纹理颜色 setRandomColors(object, scale); object.scale.set(5, 5, 5); //将模型缩放并添加到场景当中 scene.add(object); }) }); //添加辅助线 group = new THREE.Group(); up = new THREE.ArrowHelper(new THREE.Vector3(0, 1, 0), new THREE.Vector3(), 10, 0x00ff00); horizontal = new THREE.ArrowHelper(new THREE.Vector3(1, 0, 0), new THREE.Vector3(), 10, 0x00ffff); down = new THREE.ArrowHelper(new THREE.Vector3(0, -1, 0), new THREE.Vector3(), 10, 0xffff00); group.add(up); group.add(horizontal); group.add(down); //scene.add(group); } //添加纹理的方法 function setRandomColors(object, scale) { //获取children数组 var children = object.children; //如果当前模型有子元素,则遍历子元素 if (children && children.length > 0) { children.forEach(function (e) { setRandomColors(e, scale) }); } else { if (object instanceof THREE.Mesh) { //如果当前的模型是楼层,则设置固定的颜色,并且透明化 if(Array.isArray(object.material)){ for(var i = 0; i<object.material.length; i++){ var material = object.material[i]; var color = scale(Math.random()).hex(); if (material.name.indexOf("building") === 0) { material.color = new THREE.Color(color); material.transparent = true; material.opacity = 0.7; material.depthWrite = false; } } } // 如果不是场景组,则给当前mesh添加纹理 else{ //随机当前模型的颜色 object.material.color = new THREE.Color(scale(Math.random()).hex()); } } } } //初始化性能插件 function initStats() { stats = new Stats(); document.body.appendChild(stats.dom); } function initControls() { controls = new THREE.PointerLockControls( camera ); controls.getObject().position.y = 50; controls.getObject().position.x = 100; scene.add( controls.getObject() ); var onKeyDown = function ( event ) { switch ( event.keyCode ) { case 38: // up case 87: // w moveForward = true; break; case 37: // left case 65: // a moveLeft = true; break; case 40: // down case 83: // s moveBackward = true; break; case 39: // right case 68: // d moveRight = true; break; case 32: // space if ( canJump && spaceUp ) velocity.y += upSpeed; canJump = false; spaceUp = false; break; } }; var onKeyUp = function ( event ) { switch( event.keyCode ) { case 38: // up case 87: // w moveForward = false; break; case 37: // left case 65: // a moveLeft = false; break; case 40: // down case 83: // s moveBackward = false; break; case 39: // right case 68: // d moveRight = false; break; case 32: // space spaceUp = true; break; } }; document.addEventListener( 'keydown', onKeyDown, false ); document.addEventListener( 'keyup', onKeyUp, false ); } function initPointerLock() { //实现鼠标锁定的教程地址 https://www.html5rocks.com/en/tutorials/pointerlock/intro/ var havePointerLock = 'pointerLockElement' in document || 'mozPointerLockElement' in document || 'webkitPointerLockElement' in document; if ( havePointerLock ) { var element = document.body; var pointerlockchange = function ( event ) { if ( document.pointerLockElement === element || document.mozPointerLockElement === element || document.webkitPointerLockElement === element ) { controlsEnabled = true; controls.enabled = true; blocker.style.display = 'none'; } else { controls.enabled = false; blocker.style.display = 'block'; instructions.style.display = ''; } }; var pointerlockerror = function ( event ) { instructions.style.display = ''; }; // 监听变动事件 document.addEventListener( 'pointerlockchange', pointerlockchange, false ); document.addEventListener( 'mozpointerlockchange', pointerlockchange, false ); document.addEventListener( 'webkitpointerlockchange', pointerlockchange, false ); document.addEventListener( 'pointerlockerror', pointerlockerror, false ); document.addEventListener( 'mozpointerlockerror', pointerlockerror, false ); document.addEventListener( 'webkitpointerlockerror', pointerlockerror, false ); instructions.addEventListener( 'click', function ( event ) { instructions.style.display = 'none'; //全屏 launchFullScreen(renderer.domElement); // 锁定鼠标光标 element.requestPointerLock = element.requestPointerLock || element.mozRequestPointerLock || element.webkitRequestPointerLock; element.requestPointerLock(); }, false ); } else { instructions.innerHTML = '你的浏览器不支持相关操作,请更换浏览器'; } } function render() { if ( controlsEnabled === true ) { //获取到控制器对象 var control = controls.getObject(); //获取刷新时间 var delta = clock.getDelta(); //velocity每次的速度,为了保证有过渡 velocity.x -= velocity.x * 10.0 * delta; velocity.z -= velocity.z * 10.0 * delta; velocity.y -= 9.8 * 100.0 * delta; // 默认下降的速度 //获取当前按键的方向并获取朝哪个方向移动 direction.z = Number( moveForward ) - Number( moveBackward ); direction.x = Number( moveLeft ) - Number( moveRight ); //将法向量的值归一化 direction.normalize(); group.position.set(control.position.x,control.position.y,control.position.z); //判断是否接触到了模型 rotation.copy(control.getWorldDirection().multiply(new THREE.Vector3(-1, 0, -1))); //判断鼠标按下的方向 var m = new THREE.Matrix4(); if(direction.z > 0){ if(direction.x > 0){ m.makeRotationY(Math.PI/4); } else if(direction.x < 0){ m.makeRotationY(-Math.PI/4); } else{ m.makeRotationY(0); } } else if(direction.z < 0){ if(direction.x > 0){ m.makeRotationY(Math.PI/4*3); } else if(direction.x < 0){ m.makeRotationY(-Math.PI/4*3); } else{ m.makeRotationY(Math.PI); } } else{ if(direction.x > 0){ m.makeRotationY(Math.PI/2); } else if(direction.x < 0){ m.makeRotationY(-Math.PI/2); } } //给向量使用变换矩阵 rotation.applyMatrix4(m); //horizontal.setDirection(rotation); horizontalRaycaster.set( control.position , rotation ); var horizontalIntersections = horizontalRaycaster.intersectObjects( scene.children, true); var horOnObject = horizontalIntersections.length > 0; //判断移动方向修改速度方向 if(!horOnObject){ if ( moveForward || moveBackward ) velocity.z -= direction.z * speed * delta; if ( moveLeft || moveRight ) velocity.x -= direction.x * speed * delta; } //复制相机的位置 downRaycaster.ray.origin.copy( control.position ); //获取相机靠下10的位置 downRaycaster.ray.origin.y -= 10; //判断是否停留在了立方体上面 var intersections = downRaycaster.intersectObjects( scene.children, true); var onObject = intersections.length > 0; //判断是否停在了立方体上面 if ( onObject === true ) { velocity.y = Math.max( 0, velocity.y ); canJump = true; } //根据速度值移动控制器 control.translateX( velocity.x * delta ); control.translateY( velocity.y * delta ); control.translateZ( velocity.z * delta ); //保证控制器的y轴在10以上 if ( control.position.y < 10 ) { velocity.y = 0; control.position.y = 10; canJump = true; } } } //窗口变动触发的函数 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); render(); renderer.setSize( window.innerWidth, window.innerHeight ); } function animate() { //更新控制器 render(); //更新性能插件 stats.update(); renderer.render( scene, camera ); requestAnimationFrame(animate); } function draw() { //兼容性判断 if ( ! Detector.webgl ) Detector.addGetWebGLMessage(); initPointerLock(); initGui(); initRender(); initScene(); initCamera(); initLight(); initModel(); initControls(); initStats(); animate(); window.onresize = onWindowResize; } function launchFullScreen(element) { /*if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); }*/ } /*var v = new THREE.Vector3(1,0,1).normalize(); console.log(v); var m = new THREE.Matrix4(); m.makeRotationY(Math.PI/4); console.log(m); console.log(v.applyMatrix4(m)); console.log(v.multiply(new THREE.Vector3(-1, 0, -1)));*/ </script> </html>