简历
指针锁定API允许您在游戏界面中锁定鼠标或其他指针设备,以便您不用绝对定位光标就可以获得坐标变化值,从而准确地判断用户正在做什么,并且还可以防止用户意外地进入另一块屏幕或别的什么地方,从而导致误操作。
这一篇是我从官网上获得的相关控制器的,然后通过官网的案例进行一下修改扩展了一下功能。将案例实现了简单的碰撞检测。
案例实现
案例查看地址:http://www.wjceo.com/blog/threejs/2018-04-02/143.html
实现鼠标锁定
- 首先,我们在
body
里面添加了dom
标签,因为鼠标锁定只有通过用户才可以触发。
<div id="blocker">
<div id="instructions">
<span style="font-size:40px">点击屏幕开始</span>
<br />
<br />
(W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角)
</div>
</div>
- 然后,判断一下当前浏览器是否支持鼠标锁定。并绑定到了鼠标点击事件:
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();">
<div id="blocker">
<div id="instructions">
<span style="font-size:40px">点击屏幕开始</span>
<br />
<br />
(W, A, S, D = 移动, SPACE = 跳跃, MOUSE = 移动视角)
</div>
</div>
</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() {
//实现鼠标锁定的教程地址 http://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>