赛车坐标系
先不讨论场景和光线的配置,光是赛车本身涉及到的一些座标变换就足够复杂的了。先假设汽车模型摆放在坐标轴中心,如下图所示:
一般情况下坐标系为右手系表示,即图中的红,绿,蓝分别代表x,y,z轴。假设汽车要向前移动的话,那么车身和车轮都需要沿着坐标系z轴作平移变换,同时前轮需要绕着前轮车轴为中心沿着x轴方向旋转变换。直白地说,就是车子在往前走的时候轮子也需要滚动。
直行时的情况比较简单,但是如果需要旋转就比较麻烦了。先来考虑真实的情况,轮子在转动的时候不是一下子就旋转到位的,而是有个缓慢转动的过程,在转动的过程中车身也一直在前进。我们需要记录下来车身此时面对的方向以及轮子此时面对的方向。我们需要将坐标系拆分成两个部分,一个是车子和整个场景所在的世界坐标系,一个是以车子自身位中心的单位坐标系。我们需要在单位坐标系下对轮子做相对于车身的座标变换,在世界坐标系下更新车子当前的座标和面对的方向。
前期准备
实际上上面的内容Three.js里面的例子已经有现成的代码了,之前还花了好多时间研究诸如cannon.js,Physijs等等JavaSript物理引擎,出来的结果不是车子不停的打转就是动弹不得。还是先考虑没有物理引擎的情况吧。
Car.js的文件可以在Three.js的github源码中找到,在example目录下的js文件夹里面。也可以在我的github里面这个demo的代码中找到,但是我稍微修改了一下里面的参数。
首先我们需要将Three.js库以及Car.js添加到源文件当中
<script src='js/libs/three.min.js'></script>
<script src='js/Car.js'></script>
<script src="js/Detector.js"></script>
为了实现对键盘输入的检测还需要一个简单的方法监测键盘输入
var carControl = {
moveForward: false,
moveBackward: false,
moveLeft: false,
moveRight: false
}
function onKeyDown(event)
{
switch(event.keyCode)
{
case 87: /*W*/
case 38: /*up*/ carControl.moveForward = true; break;
case 83: /*S*/
case 40: /*down*/carControl.moveBackward = true; break;
case 65: /*A*/
case 37: /*left*/carControl.moveLeft = true; break;
case 68: /*D*/
case 39: /*right*/carControl.moveRight = true; break;
}
};
好了,接下来我们可以编写我们具体实现的js代码了。
构建场景元素
首先把WebGL元素添加到HTML画布中
if ( ! Detector.webgl ) {
Detector.addGetWebGLMessage();
document.getElementById( 'container' ).innerHTML = "";
}
// set the scene size
var WIDTH = window.innerWidth,
HEIGHT = 500;
container = document.getElementById("gameCanvas");
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, HEIGHT );
container.appendChild( renderer.domElement );
加入摄像机元素,这里之所以将摄像机的远截面设置得那么远是希望能够让摄像机能够捕捉到远处的场景。我们采用skybox的方式来模拟天空的场景,具体如何实现下面会提到。
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 55, window.innerWidth / HEIGHT, 0.1, 30000 );
camera.position.set( 0, 2, -7 );
camera.lookAt( new THREE.Vector3( 0, 0, 0 ) );
模拟天空场景
如何在利用Three.js模拟天空?假如直接将一个平面放置在场景的正上方,那当我们平视整个场景的时候必然是看不到任何东西的,我们需要有一种天空“包裹”住整个场景的感觉,这也许是称作skybox的原因了,可以使得画面感觉显示的范围更大。
首先要创建一个skybox的图案,用three.js的loader读取文件
// load skybox
var cubeMap = new THREE.Texture( [] );
cubeMap.format = THREE.RGBFormat;
cubeMap.flipY = false;
var loader = new THREE.ImageLoader();
loader.load( 'textures/skyboxsun25degtest.png', function ( image ) {
var getSide = function ( x, y ) {
var size = 1024;
var canvas = document.createElement( 'canvas' );
canvas.width = size;
canvas.height = size;
var context = canvas.getContext( '2d' );
context.drawImage( image, - x * size, - y * size );
return canvas;
};
cubeMap.image[ 0 ] = getSide( 2, 1 ); // px
cubeMap.image[ 1 ] = getSide( 0, 1 ); // nx
cubeMap.image[ 2 ] = getSide( 1, 0 ); // py
cubeMap.image[ 3 ] = getSide( 1, 2 ); // ny
cubeMap.image[ 4 ] = getSide( 1, 1 ); // pz
cubeMap.image[ 5 ] = getSide( 3, 1 ); // nz
cubeMap.needsUpdate = true;
} );
在回调函数中,创建了getSide函数来获取skybox立方体六个面的图案。文件本身是4096 × 3072大小的图片,利用HTML画布的绘图函数来返回每一个面图形信息,接下来就可以用Three.js自带的着色器为整个天空的skybox着色
var cubeShader = THREE.ShaderLib['cube'];
cubeShader.uniforms['tCube'].value = cubeMap;
利用生成的着色器创建skybox的材质
var skyBoxMaterial = new THREE.ShaderMaterial( {
fragmentShader: cubeShader.fragmentShader,
vertexShader: cubeShader.vertexShader,
uniforms: cubeShader.uniforms,
depthWrite: false,
side: THREE.BackSide
});
接下来就可以创建skybox物体并添加到场景当中去了
var skyBox = new THREE.Mesh(
new THREE.BoxGeometry( 10000, 10000, 10000 ),
skyBoxMaterial
);
scene.add( skyBox );
添加地面
城市模型大小有限,难以覆盖到整个画面,因此需要在场景中加入一个平面。
//add ground
var grassTex = THREE.ImageUtils.loadTexture('textures/grass.png');
grassTex.wrapS = THREE.RepeatWrapping;
grassTex.wrapT = THREE.RepeatWrapping;
grassTex.repeat.x = 256;
grassTex.repeat.y = 256;
var groundMat = new THREE.MeshBasicMaterial({map:grassTex});
var groundGeo = new THREE.PlaneGeometry(4000,4000);
ground = new THREE.Mesh(groundGeo,groundMat);
ground.position.y = -3; //lower it
ground.rotation.x = -Math.PI/2; //-90 degrees around the xaxis
ground.doubleSided = true;
ground.receivedShadow = true;
scene.add(ground);
由于地面需要极大的图片,而grass.png大小比较小,这里需要设置图片自身要重复多次,因此wrapS和wrapT的值都设为真,同时设置在x,和y,方向上均需要重复256次。地面的geometry由THREE.PlaneGeometry创建,并且在y轴上往负方向平移了-3个单位,给车的模型留出空位。而之所以绕着x轴旋转90度是因为原始平面是沿着x,y轴平面的,需要旋转至x,z轴平面。
加入汽车和城市模型
添加汽车到场景中的方式很简单,只需要给函数需要的信息就够了,同时也需要写好回调函数
car = new THREE.Car();
car.modelScale = 1;
car.backWheelOffset = 2;
car.callback = function( object ) {
var x = 270;
var y = 0.1;
var z = -330;
// var s = 0.01;
object.root.position.set( x, y, z );
scene.add( object.root );
object.root.add( camera );
camera.position.set( 0, 10, -30 );
object.bodyMesh.geometry.dynamic = true;
object.bodyMesh.geometry.computeFaceNormals();
}
car.setVisible( true );
car.loadPartsJSON( './models/Audi_Car/audi_body.js', './models/Audi_Car/wheel_left.js' );
这里的回调函数起着设定汽车的初试状态的作用,因为要配合城市模型,所以x,y,z起始座标都需要更改。同时为了保证摄像头能够一直跟踪汽车,将摄像头加入到汽车的单位坐标系当中,位置也设置好。
object.bodyMesh.geometry.computeFaceNormals();
这一句代码的作用是计算汽车每个面的法向量,根据这个法向量Three.js在着色的时候就不用Flat Shading,而是采用更为平滑的Smooth Shading。
oadPartJSON函数读取js格式下的模型,可以利用官方Three.js写好的python脚本将常用的.obj,.fbx等等格式转换成.js格式,脚本文件存放在Three.js源代码目录下的utils/converters文件夹里面。这里面要注意,车身的模型存放在在audi_body.js文件里,wheel_left.js只存放了车子的左前轮模型。Car.js会自己计算剩下的三个轮子的座标,只需要保证左前轮的模型仍然保持在车子模型的坐标系里面左前轮的位置,不要将轮子的座标放到坐标原点。
将城市的模型也放到场景当中,整个游戏的场景就算是差不多搭建完成了。
new THREE.JSONLoader().load( './models/City_4/City.js', function( geometry, material ) {
geometry.computeVertexNormals();
var cityMesh = new THREE.Mesh( geometry, new THREE.MeshFaceMaterial( material ) );
cityMesh.scale.set( 4, 4, 4 );
cityMesh.rotation.set( 0, Math.PI / 2, 0 );
cityMesh.position.set( -0, -2.9, 400 );
scene.add( cityMesh );
});
接下来只需要完成定时刷新网页的函数并加入到执行函数当中。
function animate() {
requestAnimationFrame(animate);
car.updateCarModel( Date.now() - time, carControl );
render();
time = Date.now();
}
function render() {
renderer.render( scene, camera );
}
最终效果
游戏截图如下:
模型的加载时间对于网页而言可能有些慢,并且由于需要渲染的场景比较大会有一定的延时,不过看到自己的完成的模型能够在画面中跑起来还是挺有趣的。我已经上传了这个程序的源代码到github上,网址是:CityRun。也可以直接点击这个链接直接尝试一下最后的做出来的游戏。
Comments !