使用 WebGL 进行 3D 开发第 3 部分 添加用户交互.docx
《使用 WebGL 进行 3D 开发第 3 部分 添加用户交互.docx》由会员分享,可在线阅读,更多相关《使用 WebGL 进行 3D 开发第 3 部分 添加用户交互.docx(19页珍藏版)》请在冰点文库上搜索。
使用WebGL进行3D开发第3部分添加用户交互
使用WebGL进行3D开发,第3部分添加用户交互
在IBMBluemix云平台上开发并部署您的下一个应用。
开始您的试用
硬件加速的3D性能是目前每一个JavaScript开发都关注的事情,因为桌面和移动浏览器中对WebGL的支持几乎无处不在。
在此WebGL系列的第2部分,我们使用了以下两个高级别的WebGL库进行实验:
Three.js和SceneJS。
您看到了这些库如何通过简单的、概念性的纯API来封装原始WebGL开发的复杂性,并实现快速的3D开发。
您学习了如何:
用10行Three.js代码而不是100行原始WebGL来旋转金字塔通过Three.js类利用面向对象的设计,迅速克隆3D对象,并创建可以显示多个运动中对象的场景使用Three.js学习基本的3D概念,包括场景图、网格、素材、灯光和摄像机的放置创建场景中的轮廓式对象的动画通过编程实现一个组合了多种运动的场景—对象固有的运动和整组对象的运动通过运用灯光和阴影效果,创建激动人心的场景使用照片般逼真的纹理,通过纹理贴图增加被渲染对象的真实感使用Three.js几何形状API来生成不规则形状的3D几何图形处理3D对象的偏心旋转从在线3D仓库/存储库中获得复杂的、渲染就绪的预制3D网格和纹理,并将它们包含在您的3D场景中通过动画补间(tweening)功能来规划多区域3D空间的飞行可视化,为其划分阶段和编写代码,并通过easing函数来增强效果使用SceneJS库轻松创建复杂的3D场景图形,并对比SceneJS的设计与Three.js库立刻创建3D应用程序和数据可视化,将目前所学的技巧付诸于实践。
此系列的最后这一篇文章将介绍3D用户交互的一些概念和技术,然后引导您完成两个3D应用程序的开发:
一个完整的3D井字过三关游戏,以及一个交互式数据可视化用户界面。
请参阅下载,以获得样例代码。
在本文结束时,您就会掌握在您即将到来的Web开发项目中应用WebGL和处理3D需求的足够知识。
3D场景的用户交互在此文章系列中,目前为止已经构建了包含3D动画对象和移动摄像机(飞过或走过)的场景,但使用的都是用户不能参与的预先编程的动作。
用户体验一直类似于观看视频。
在实践中,许多3D应用程序(以及其他许多游戏和数据可视化)需要与用户进行交互。
利用Three.js等高层次的WebGL库来增加用户交互可能非常简单。
在浏览器中加载imatlight.html。
这是第2部分中提供的灯光和阴影效果示例(matlight.html),添加了摄像头位置的3D用户控制。
图1显示了由imatlight.html渲染的场景。
图1.可与用户进行交互的3D场景(在OSX上的Safari中)开始与场景进行交互。
通过使用鼠标,您可以:
水平或垂直平移场景:
单击并按住鼠标右键,同时移动鼠标。
进入或离开场景,以显示更多或更少的细节:
单击并按住鼠标滚轮(或中心),同时移动鼠标。
您也可以通过转动鼠标滚轮来实现同样的效果。
围绕场景运动,保持与场景的当前距离:
单击并按住鼠标左键,同时移动鼠标。
通过这些运动和一些练习,您应该能够有效地、快速地在3D渲染的场景中实现导航。
图2显示了进行一些交互后的imatlight.html。
您将从完全不同的角度查看图1的场景。
图2.进行用户交互后的另一个角度的imatlight.html(在OSX上的Chrome中)清单1显示了imatlight.html的代码。
突出显示的行是添加到matlight.html(来自第2部分)的代码,用于将用户交互融入到场景中。
清单1.与3D场景(imatlight.html)进行交互
developerWorksWebGLThree.jsInteractiveLightsandShadowsEffectExample
functiondraw3D(){
varcontrols;
functionanimate(){
requestAnimationFrame(animate);
pyramid1.rotateY(Math.PI/180);
sphere.rotateY(Math.PI/180);
cube.rotateY(Math.PI/180);
multi.rotateY(Math.PI/480);
renderer.render(scene,camera);
}
functionupdateControls(){
controls.update();
}
vargeo=newTHREE.CylinderGeometry(0,2,2,4,1,true);
varpyramid1=newTHREE.Mesh(geo,newTHREE.MeshPhongMaterial({color:
0xff0000}));
pyramid1.position.set(-2.5,-1,0);
geo=newTHREE.SphereGeometry(1,25,25);
varsphere=newTHREE.Mesh(geo,newTHREE.MeshPhongMaterial({color:
0x00ff00}));
sphere.position.set(2.5,-1,0);
geo=newTHREE.CubeGeometry(2,2,2);
varcube=newTHREE.Mesh(geo,newTHREE.MeshPhongMaterial({color:
0x0000ff}));
cube.position.set(0,1,0);
varcamera=newTHREE.PerspectiveCamera(45,1024/500,0.1,100);
camera.position.z=10;
camera.position.y=1;
controls=newTHREE.OrbitControls(camera);
controls.addEventListener('change',updateControls);
varmulti=newTHREE.Object3D();
pyramid1.castShadow=true;sphere.castShadow=true;
multi.add(cube);
multi.add(pyramid1);
multi.add(sphere);
multi.position.z=0;
geo=newTHREE.PlaneGeometry(20,25);
varfloor=newTHREE.Mesh(geo,newTHREE.MeshBasicMaterial({color:
0xcfcfcf}));
floor.material.side=THREE.DoubleSide;
floor.rotation.x=Math.PI/2;
floor.position.y=-2;
floor.receiveShadow=true;
varlight=newTHREE.DirectionalLight(0xe0e0e0);
light.position.set(5,2,5).normalize();
light.castShadow=true;
light.shadowDarkness=0.5;
light.shadowCameraRight=5;
light.shadowCameraLeft=-5;
light.shadowCameraTop=5;
light.shadowCameraBottom=-5;
light.shadowCameraNear=2;
light.shadowCameraFar=100;
varscene=newTHREE.Scene();
scene.add(floor);
scene.add(multi);
scene.add(light);
scene.add(newTHREE.AmbientLight(0x101010));
vardiv=document.getElementById("shapecanvas2");
varrenderer=newTHREE.WebGLRenderer();
renderer.setSize(1024,500);
renderer.setClearColor(0x000000,1);
renderer.shadowMapEnabled=true;
div.appendChild(renderer.domElement);
animate();
}
面向Three.js的OrbitControlsAPI可选的Three.js控制APIThree.js代码分发包含用于3D硬件和用例的无数可供选择的用户输入/输出支持API,这些API都位于源代码目录的examples/js/controls目录中。
TrackballControls.js通过一个轨迹球支持用户交互。
FirstPersonControl.js支持许多第一人称视角(FPV)游戏玩家所熟悉的用户交互模式。
FlyControl.js支持飞行模拟器式摄像机控制,支持滚动和俯仰。
OculusControls.js通过OculusRift支持用户交互,OculusRift是一个仍在开发中(并备受期待)的、精密的消费者头部跟踪沉浸式虚拟现实设备。
在Three.js中,是通过OrbitControls.jsAPI来支持鼠标交互的,该API位于Three.js源代码树的examples/js/controls目录。
因为不是所有3D应用程序都要求用来与其他一些硬件设备进行交互的用户交互、OrbitControls和其他API是可选的库(参阅可选的Three.js控制API边栏)。
OrbitControls的工作原理是在3D场景内与鼠标输入一致地移动摄像机的位置。
下面的两行代码举例说明了该控制,并使用来自3D场景的摄像机将其参数化:
controls=newTHREE.OrbitControls(camera);
controls.addEventListener('change',updateControls);OrbitControlschange侦听器对updateControls()函数进行了回调,该函数的定义是:
functionupdateControls(){
controls.update();
}在imatlight.html3D场景中,animate()回调函数已经通过一个rAF钩在刷新屏幕时更新对象旋转;这就是为什么
updateControls()只需要调用
controls.update()。
如果被渲染的场景是静态的,那么rAF不会被钩住,而且只有在检测到控制变化时才会进行渲染。
在这种情况下,updateControls()函数还应该调用渲染器的渲染函数来更新场景。
回页首设计3D游戏您要开发的下一个项目是一个需要用户交互的完全可玩的3D游戏:
3D井字过三关游戏。
两名对手玩家持不同颜色的棋子。
一个玩家的棋子是红色的,另一个玩家的棋子是绿色的。
玩家轮流将自己的棋子放在一个3x3x3的立方体“井字框架(cage)”里面。
三个棋子沿任意维度排成一行的第一个玩家将会获得胜利。
这里给出的代码实现了一个电脑玩家(控制红色棋子),您可以与其进行对战。
核心“引擎”代码是独立的,可以修改为人与人对战的游戏,也许可以通过网络实现对战。
图3显示了玩这个游戏用的3D立方体。
您可以看到3x3x3=27个位置,棋子可以放在这些位置上,所有位置都由白球占据。
图3.3D井字过三关游戏的比赛场地(在OSX上的Firefox中)当玩家围绕“井字框架”运动,思考下一步操作的时候,鼠标经过的每个可用位置都将变为黄色。
然后,玩家可以单击选定的球,提交一步操作,球体就会变为该玩家的颜色。
屏幕上显示的文字指出了这是哪一个玩家的操作,并在其中一个玩家获胜时显示相关文字。
图4显示了红色代表的计算机玩家的获胜组合,以及最终的屏幕显示。
在获得胜利后,可单击屏幕上任意位置来重置游戏。
图4.红色玩家的胜利(在OSX上的Chrome中)加载tictacthreed.html,并自己试着玩一下这个游戏。
就像在imatlight.html中那样,您可以使用鼠标围绕游戏的井字框架运动。
(两个页面都使用了Three.jsOrbitControlsAPI。
)计算机玩家并不是特别聪明,您可以很轻松地赢得游戏。
在游戏结束时,单击任意位置再次启动一个新游戏。
前一次比赛中的失败方在新游戏中可以先走第一步。
在试玩几场比赛后,查看一下tictacthreed.html的源代码(参见下载)。
清单2中的代码是tictacthreed.html的部分,用于创建放棋子的3D游戏井字框架。
清单2.创建3D游戏井字框架varbase=newTHREE.Geometry();
for(varz=-1;z<1;z++){
base.vertices.push(
newTHREE.Vector3(0,0,z),newTHREE.Vector3(3,0,z),
newTHREE.Vector3(0,1,z),newTHREE.Vector3(3,1,z),
newTHREE.Vector3(1,2,z),newTHREE.Vector3(1,-1,z),
newTHREE.Vector3(2,2,z),newTHREE.Vector3(2,-1,z)
);
}
for(varx=1;x<3;x++){
base.vertices.push(
newTHREE.Vector3(x,1,1),newTHREE.Vector3(x,1,-2),
newTHREE.Vector3(x,0,1),newTHREE.Vector3(x,0,-2)
);
}
varcage=newTHREE.Line(base,newTHREE.LineBasicMaterial(),THREE.LinePieces);
cage.position.set(-1.5,-0.5,0.5);共有12条相交的直线形成这个井字框架。
前四条线在x-y平面上。
第二组的四条线的结束坐标与第一组的相同,但其z分量是-1,而不是0。
最后四条线包含两组线条,它们的结束坐标除了x值外都一样。
使用THREE.Line构造函数创建由线“段”(彼此没有连接的线段)组成的cage。
在构造函数后面,转换已完成的cage,让它的中心位于原点(0,0,0)上。
生成标志着游戏位置的球体为了生成用于标志可用位置的白色球体,tictacthreed.html使用了清单3中所示的迭代代码。
清单3.生成白色球体vargeo=newTHREE.SphereGeometry(0.3,25,25);
varrange=[-1,0,1];
varidx=0;
range.forEach(function(x){
range.forEach(function(y){
range.forEach(function(z){
vartempS=newTHREE.Mesh(geo,newTHREE.MeshPhongMaterial({color:
0xffffff}));
tempS.ID=idx++;
tempS.claim=UNCLAIMED;
pos.push(tempS);
tempS.position.set(x,y,z);
scene.add(tempS);
})
)
});计算机玩家的实现通过redComputerMove()函数实现计算机玩家。
每当轮到红色玩家的时候,都会调用redComputerMove()来走一步。
这个函数首先会在wins数组中扫描所有获胜组合,以确定它是否能够在下一步中获胜。
如果不能,那么它会重新扫描组合,以确定它是否必须阻止绿色玩家即将出现的胜利(因为绿色已占据一个获胜组合中的两个位置,剩下一个还没有人占据)。
由countClaim()辅助函数协助完成获胜组合的扫描。
如果redComputerMove()不能赢,也不需要进行阻止,那么它会通过遍历preferred位置数组来确定下一步的行动,或者占据第一个可用的无人占据的位置。
按照这一策略,计算机会“合理”地进行下棋,但无法赢得每一场比赛。
当然,您可以改进游戏策略。
球体的直径都是0.6(半径为0.3),因此用户可以透过井字框架看到它们。
清单3中的代码创建了白色球体,并将它们放置到所有27个可玩的位置。
需要注意的是,虽然所有27个球体都使用相同的几何对象(这些对象被命名为geo)进行网状构造,但每个球体都有一个单独的实例THREE.Material。
这是必需的,因为代码会在后面的操作中单独更改每个球体的颜色。
如果所有的网格实例都引用了同一个素材,那么所有球体都将同时改变颜色。
清单3的代码还建立了一个名为pos的数组,它引用27个网格,一个球体引用一个网格。
胜者决定算法使用了pos数组来检查是否有玩家已经获胜(并重置游戏)。
计算机玩家的代码也会大量使用pos数组来确定计算机玩家目前是否正在受到威胁,或者是否应该走出攻击性的一步。
每个球体网格对象都有一个额外的属性,?
?
名为claim,该属性被初始化为UNCLAIMED。
当用户在相关的可玩位置/球体提交一步操作时,该属性被更改为RED或GREEN。
图5显示了由清单3的代码生成的游戏位置的编号方案。
每个数字代表该游戏位置(球体网格)在所生成的pos数组中的索引。
胜者决定算法使用了这些指标集来判断玩家是否获胜。
图5.游戏位置编号方案决定胜方游戏中有49个可能的获胜组合,每个组合有三个位置。
您可以根据图5手动枚举这些组合。
在tictacthreed.html中,wins数组包含了所有获胜组合的枚举。
为了判断某个玩家是否获胜,checkWin(playerColor)函数将会遍历每个组合,并检查组合中的所有三个球体是否使用了玩家的颜色。
通过检查组合中的每个球体的claim属性来确定胜方;当玩家单击选中的球体时,该属性被设置为该玩家的颜色。
清单4显示了checkWin()的代码。
清单4.checkWin()函数functioncheckWin(color){
varwon=false;
varbreakEx={};
try{
wins.forEach(function(wincomb){
varcount=0;
wincomb.forEach(function(idx){
if(pos[idx].claim==color)
count++;
})
if(count===3){
won=true;
throwbreakEx;
}
})
}catch(ex){
if(ex!
=breakEx)throwex;
}
returnwon;
}在清单4突出显示的代码中,在确定胜方后,checkWin()通过抛出一个异常来切断forEach()循环,并将true状态返回给调用程序。
使用2D鼠标在3D场景中拾取对象3D的另一个重要的用户交互技术是对象拾取,即3D场景中的对象选择。
在井字过三关游戏中,输入设备是一个2D鼠标。
用户实际单击的是在其中渲染3D场景的画布。
因为当用户周绕场景运动时,渲染会发生变化,必须将鼠标的2D坐标动态地(在鼠标单击时)映射到场景的三维坐标空间,以确定哪些对象被选中。
在2D图形中,通过命中测试来执行鼠标选择。
对象拾取是3D中的一种命中测试形式。
Three.js通过提供一个projector辅助程序来简化对象拾取,它可以从2D画布(x,y)点过渡到场景的3D世界,同时还会考虑到当前摄像机的属性(摄像机所指的方向和角度等)。
Three.js也有一个RayCaster类,可以将光线投射到3D场景中,并确定光线是否与场景中指定的3D对象集合相交。
在井字过三关游戏中,在屏幕更新期间执行命中测试。
鼠标移动事件侦听程序会将鼠标的x和y坐标保存为一个全局变量。
在命中测试中,与Raycaster相?
?
交的第一个对象用黄色(RGBHex
0xffd700)突出显示,告诉用户该位置是可用的。
清单5显示了执行这一命中测试的tictacthreed.html中的代码。
清单5.命中测试functionupdateControls(){
varvector=newTHREE.Vector3(mouse.x,mouse.y,0.5);
projector.unprojectVector(vector,camera);
varray=newTHREE.Raycaster(camera.position,
vector.sub(camera.position).normalize());
varhits=ray.intersectObjects(pos);
if(mouse.clicked){
...
}
else{/*mousemove*/
if(hits.length>0)
{
...
}
else
{
...;
}
}
}图形sprite图形sprite是在2D图形动画中使用的基本元素。
它们一般是经过优化的预渲染图形的矩形块,用于在2D加速的硬件平台上快速显示和动画。
在3D渲染的上下文中,sprite指含有2D绘图/图形的矩形平面2D几何图形,比如在3D场景中的2D文本标签。
在执行命中测试后,hits变量包含THREE.RayCaster在场景内发现相交的球体的列表(来自pos)。
如果列表不为空,那么所返回的数组中的第一个对象就是第一个与投射光线相交的对象(“最上面的”球体)。
当用户单击一个可玩的位置时,在该位置上的球体就会变成玩家的颜色。
该球体的claim属性也被更新,以反映玩家所走的这一步。
清单6