主要面向Web前端工程师,须要必定Javascript及three.js基础;
本文主要分享内容为基于three.js开发WebVR思路及碰到的问题;
有兴趣的同窗,欢迎跟帖讨论。javascript
目录:
1、项目体验
1.一、项目简介
1.二、功能介绍
1.三、游戏体验
2、技术方案
2.一、为何使用WebVR
2.二、经常使用的WebVR解决方案
2.2.一、Mozilla的A-Frame方案
2.2.二、three.js及webvr-polyfill方案
3、技术实现
3.一、知识储备
3.二、实现步骤
3.三、工做原理
4、技术难点
4.一、程序与用户共同控制摄像头
4.二、多重蒙板贴图
4.三、镜头移动
4.四、3d自适应长度文字提示
4.五、unity3d地形导出
4.六、3dmax动画导出问题
5、完整的源代码及相应组件html
1、项目体验
1.一、项目简介:
1.1.一、名称:
“重历阿尔特里亚”——龙之谷手游手首发ChinaJoy2016预热VR小游戏
前端
1.1.二、开发背景:
基于龙之谷手游具有的3D属性,全景视角体验,以及ChinaJoy首发的线下场景,咱们和品牌讨论除了基于VR的线下体验项目。因为基于Web技术较好的兼容性、开发的高效性,咱们采用了WebVR技术来实现整个体验。java
1.1.三、使用WebVR优点:
1.1.3.一、普通web前端工程师能够参与VR应用开发,下降了开发门槛;
1.1.3.二、跨设备终端、跨操做系统、跨APP载体;
1.1.3.三、开发快速、维护方便、随时调整、传播便捷;
1.1.3.四、浏览器便可体验,无需安装。git
1.二、功能介绍
基于游戏内3D场景、人物和道具模型,经过WebGL框架three.js开发的VR小游戏,在ChinaJoy龙之谷手游展台给玩家提供线下VR互动体验,并在后续应用于线上营销传播。不具有VR眼镜设备的用户可选择普通模式进行互动体验。github
1.三、游戏体验
若是你身边正好有VR眼镜,请选择VR模式体验;若是没有,请选择普通模式。
须要说明的是,因为本次应用针对线下场景,而合做方三星提供了最新的S7手机和GearVR设备,因此项目只针对S7作了体验优化,因此可能部分手机会有卡顿或者3D模型错乱的状况。
你能够扫描以下二维码或打开http://dn.qq.com/act/vr/进行体验:web
2、技术方案
2.一、为何是时候尝试WebVR了?
2.1.一、时机慢慢成熟,咱们经过几件事件便可感知:
2015年初,Mozilla在firefox nightly增长了对WebVR的支持;
2015年末,MozVR团队推出开源框架A-Frame,能过HTML标签,便可建立VR网页;
2015年末,Egret3D发布,开发团队称将在之后版本中实现WebVR的支持;
2016年初,Google与Mozilla联合建立WebVR标准;
2016年6月,Google计划将整个Chrome浏览器搬进VR世界中。
2.1.二、WebVR开发成本更低。
2015年VR硬件迅速发展,但时至今日,VR内容仍是稍显单薄。缘由在于,VR开发成本太高,而WebVR依托于WebGL及相似threeJS等框架,大大下降开发者进入VR领域的门槛。
2.1.三、Web自身的优点
上文中已有说起,依托也Web,具备不需安装、便于传播、便于快速迭代等特色。json
2.二、目前阶段,经常使用的WebVR解决方案:
2.2.一、A-frame
介绍:Mozilla的开源框架,经过定制HTML元素便可构建WebVR方案的框架,适用于没有webGL与threeJS基础的初学者。
优势:基于threeJS的封装,经过特定的标签就可以快速建立VR网页;
缺点:所提供的组件有限,难以完成较复杂的项目。
实例:
2.2.1.一、建立一个简单的场景。canvas
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="description" content="Composite — A-Frame"> <script src="../aframe.js"></script> </head> <body> <a-scene> <!-- 环境光. --> <a-entity light="type: ambient; color: #888"></a-entity> <a-entity position="0 2.2 4"> <!-- 添加相机 --> <a-entity camera look-controls wasd-controls> <!-- 添加圆环 --> <a-entity cursor geometry="primitive: ring; radiusOuter: 0.015; radiusInner: 0.01; segmentsTheta: 32" material="color: #283644; shader: flat" raycaster="far: 30" position="0 0 -0.75"></a-entity> </a-entity> </a-entity> </a-scene> </body> </html>
源码讲解:
如上简单的几个标签,便可构建一个包含灯光、相机、跟随相机的物体的场景,其他事情,都将由A-frame进行解析,具体标签与属性很少做讲解,能够参考 A-frame DOC。浏览器
2.2.1.一、加载一个由软件(好比3dmax)导出的模型。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="description" content="Composite — A-Frame"> <script src="../aframe.js"></script> <script> AFRAME.registerComponent('json-model', { schema: { type: 'src' }, init: function () { this.loader = new THREE.JSONLoader(); }, update: function () { var mesh = this.el.getOrCreateObject3D('mesh', THREE.Mesh); this.loader.load(this.data, function (geometry) { mesh.geometry = geometry; }); } }); </script> </head> <body> <a-scene> <a-assets> <a-asset-item id="sculpture" src="data/building-ground.js"></a-asset-item> </a-assets> <a-entity id="car" json-model="#sculpture" position="0 0 0" scale="5 5 5" rotation="0 45 0" material="src: url(cross-domain/skin/xianxiasq_zhujianqiangmian_001.png)"></a-entity> </a-scene> </body> </html>
源码讲解:
这个例子主要演示,A-Frame如何添加组件,对,由于A-Frame现阶段组件太少,加载自定义模式须要本身扩展组件。而组件添加须要three.js基础。
so,A-Frame出发点是很是美好的,学习几个简单的标签及属性,便可以搭建3d/webvr场景,可是现实倒是目前它还并不成熟,而且伴随着A-Frame主设计师跳槽到Google,因此我很早就放弃这个方案了。
二、基于threeJS与webVR组件,事实上,A-frame就是基于这二者的封装。
优势:能够完成复杂项目,能够结合原生的webGL;
缺点:须要掌握threeJS,须要了解webGL,学习成本较高。
在本项目中,选用的就是这个方案,在下章节中,将会进行详细介绍。
3、技术实现
3.一、知识储备:
three.js(掌握)、webGL(了解)、javascript
对three.js没有基础的同窗,能够移步至 Three.js实例教程
3.二、实现步骤:
简单来讲,完成一个WebVR应用,须要如下三个步骤:
3.2.一、搭建场景
如上图与示:
首先咱们须要载入咱们的资源,这些资源包括地形、角色、动画、及辅助元素;
而后建立咱们须要的元素,好比灯光、相机、天空等;
而后完成主业务逻辑。
3.2.二、交互
即用户的动做输入,这些动做包括:
位置移动、旋转、视线焦点、声音、甚至全身全部关节动做。
固然,当前咱们可利用的硬件设备有限,手机自身可利用的如陀螺仪、罗盘、听筒。其他辅助设备经常使用如Leap Motion、Kinect等。
更多的额外设备意识着更高的使用成本,在本案例中使用的到的动做输入信息:
用户当前方向,由VRControls.js与webvr-polyfill.js实现完成;
用户视角焦点,完成按钮点击、攻击等动做,经过跟随相机的物体检测碰撞来完成。
3.2.三、分屏
如上图所示,为让用户更具沉侵感,一般会根据用户瞳距将屏幕分割成具备必定视差的两部分,勿需担忧,这部分工做由VREffect.js来完成。
3.三、工做原理
上节中提到了webvr相关组件,原本咱们能够简单利用它提供的接口就能够完成,但确定仍是有同窗会好奇,它的工做原理是怎样的呢。
这得从Mozilla与Google 2016年初联手推出的WebVR API提案开始,WebVR Specification,该提案给VR硬件定义了专门定制的接口,让开发者可以构建出沉浸感强,温馨度高的VR体验。但因为该标准还处于草案阶段,因此咱们开发须要WebVR Polyfill,这个组件不须要特定浏览器,就可使用WebVR API中的接口。
因此咱们只须要在项目中,引入webvr-polyfill.js及VRControls、VREffect两个类,并调用便可。
vrEffect = new THREE.VREffect(renderer); vrControls = new THREE.VRControls(camera);
webvr-polyfill基于普通浏览器实现了WebVR API 1.0功能;
VRControls更新摄像头信息,让用户以第一人称置于场景中;
VREffect负责分屏。
4、技术难点
4.一、程序与用户共同控制摄像头
当程序在自动移动镜头的过程当中,容许用户四处观察,这时候须要一个辅助容器共同控制镜头旋转与移动。
// 添加摄像机 camera = new THREE.PerspectiveCamera(60, size.w / size.h, 1, 10000); camera.position.set(0, 0, 0); camera.lookAt(new THREE.Vector3(0,0,0)); // 辅助镜头移动 dolly = dolly = new THREE.Group(); dolly.position.set(10, 40, 40); dolly.rotation.y = Math.PI/10; dolly.add(camera); scene.add(dolly);
4.二、多重蒙板贴图
如上图所示,该地形由三种贴图经过蒙板共同合成,这时候咱们须要使用自定义Shader来实现,由rbg三个通道控制显示。
核心代码(片元着色器):
fragmentShader: [ 'uniform sampler2D texture1;', 'uniform sampler2D texture2;', 'uniform sampler2D texture3;', 'uniform sampler2D mask;', 'void main() {', 'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);', 'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);', 'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);', 'vec4 colorMask = texture2D(mask, vUv);', 'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;', 'gl_FragColor = vec4(outgoingLight, 1.0);', '}' ].join("\n")
完整代码(添加three.js灯光,雾化):
// 合成材质 var map1 = texLoader.load('cross-domain/skins/foor_stone02.png' ); var map2 = texLoader.load('cross-domain/skins/green_wet09.png'); var map3 = texLoader.load('cross-domain/skins/stone_dry02.png'); // 自定义复合蒙板shader THREE.FogShader = { uniforms: lib.extend( [ THREE.UniformsLib[ "fog" ], THREE.UniformsLib[ "lights" ], THREE.UniformsLib[ "shadowmap" ], { 'texture1': { type: "t", value: map1}, 'texture2': { type: "t", value: map2}, 'texture3': { type: "t", value: map3}, 'mask': { type: "t", value: texLoader.load('cross-domain/skins/mask.png')} } ] ), vertexShader: [ "varying vec2 vUv;", "varying vec3 vNormal;", "varying vec3 vViewPosition;", THREE.ShaderChunk[ "skinning_pars_vertex" ], THREE.ShaderChunk[ "shadowmap_pars_vertex" ], THREE.ShaderChunk[ "logdepthbuf_pars_vertex" ], "void main() {", THREE.ShaderChunk[ "skinbase_vertex" ], THREE.ShaderChunk[ "skinnormal_vertex" ], "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", "vUv = uv;", "vNormal = normalize( normalMatrix * normal );", "vViewPosition = -mvPosition.xyz;", "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );", THREE.ShaderChunk[ "logdepthbuf_vertex" ], "}" ].join('\n'), fragmentShader: [ 'uniform sampler2D texture1;', 'uniform sampler2D texture2;', 'uniform sampler2D texture3;', 'uniform sampler2D mask;', 'varying vec2 vUv;', 'varying vec3 vNormal;', 'varying vec3 vViewPosition;', // "vec3 outgoingLight = vec3( 0.0 );", THREE.ShaderChunk[ "common" ], THREE.ShaderChunk[ "shadowmap_pars_fragment" ], THREE.ShaderChunk[ "fog_pars_fragment" ], THREE.ShaderChunk[ "logdepthbuf_pars_fragment" ], 'void main() {', THREE.ShaderChunk[ "logdepthbuf_fragment" ], THREE.ShaderChunk[ "alphatest_fragment" ], 'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);', 'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);', 'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);', 'vec4 colorMask = texture2D(mask, vUv);', 'vec3 normal = normalize( vNormal );', 'vec3 lightDir = normalize( vViewPosition );', 'float dotProduct = max( dot( normal, lightDir ), 0.0 ) + 0.2;', 'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;', THREE.ShaderChunk[ "shadowmap_fragment" ], THREE.ShaderChunk[ "linear_to_gamma_fragment" ], THREE.ShaderChunk[ "fog_fragment" ], // 'gl_FragColor = vec4( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b, 1.0 ) + vec4(outgoingLight, 1.0);', // 'gl_FragColor = outgoingLight;', 'gl_FragColor = vec4(outgoingLight, 1.0);', '}' ].join("\n") }; THREE.FogShader.uniforms.texture1.value.wrapS = THREE.FogShader.uniforms.texture1.value.wrapT = THREE.RepeatWrapping; THREE.FogShader.uniforms.texture2.value.wrapS = THREE.FogShader.uniforms.texture2.value.wrapT = THREE.RepeatWrapping; THREE.FogShader.uniforms.texture3.value.wrapS = THREE.FogShader.uniforms.texture3.value.wrapT = THREE.RepeatWrapping; var material = new THREE.ShaderMaterial({ uniforms : THREE.FogShader.uniforms, vertexShader : THREE.FogShader.vertexShader, fragmentShader : THREE.FogShader.fragmentShader, fog: true });
三、 镜头移动(依赖Tween类)
功能函数:
cameraTracker: function(paths){ var tweens = []; for(var i = 0; i < paths.length; i++) { (function(i){ var tween = new TWEEN.Tween({pos: 0}).to({pos: 1}, paths[i].duration || 5000); tween.easing(paths[i].easing || TWEEN.Easing.Linear.None); tween.onStart(function(){ var oriPos = dolly.position; var oriRotation = dolly.rotation; this.oriPos = {x: oriPos.x, y: oriPos.y, z: oriPos.z}; this.oriRotation = {x: oriRotation.x, y: oriRotation.y, z: oriRotation.z}; }); tween.onUpdate(paths[i].onupdate || function(){ if(paths[i].pos) { dolly.position.x = this.oriPos.x + this.pos * (paths[i].pos.x - this.oriPos.x); dolly.position.y = this.oriPos.y + this.pos * (paths[i].pos.y - this.oriPos.y); dolly.position.z = this.oriPos.z + this.pos * (paths[i].pos.z - this.oriPos.z); } if(paths[i].rotation) { dolly.rotation.x = this.oriRotation.x + this.pos * (paths[i].rotation.x - this.oriRotation.x); dolly.rotation.y = this.oriRotation.y + this.pos * (paths[i].rotation.y - this.oriRotation.y); dolly.rotation.z = this.oriRotation.z + this.pos * (paths[i].rotation.z - this.oriRotation.z); } }); tween.onComplete(function(){ paths[i].fn && paths[i].fn(); var fn = tweens.shift(); fn && fn.start(); }); tweens.push(tween); })(i); } tweens.shift().start(); }
调用:
lib.cameraTracker([ {'pos': { x: -45,y: 5, z: -38},'rotation': {x: 0, y: -1.8, z: 0}, 'easing': TWEEN.Easing.Cubic.Out,'duration':4000} ]);
四、自适应长度文字提示
根据文字长度生成canvas做为贴图到Sprite对象。
hint = function(text, type, posY, fadeTime){ var chinense = text.replace(/[u4E00-u9FA5]/g, ''); var dbc = chinense.length; var sbc = text.length - dbc; var length = dbc * 2 + sbc; var fontsize = 40; var textWidth = fontsize* length / 2; posY = posY || 0.3; type = type || 1; fadeTime = fadeTime === window.undefined ? 500 : fadeTime; if(text == 'sucess' || text == 'fail') { text = ' '; } var canvas = document.createElement("canvas"); var width = 1024, height = 512; canvas.width = width; canvas.height = height; var context = canvas.getContext('2d'); var imageObj = document.querySelector('#img-hint-' + type); context.drawImage(imageObj, width/2 - imageObj.width/2, height/2 - imageObj.height/2); context.font = 'Bold '+ fontsize +'px simhei'; context.fillStyle = "rgba(255,255,255,1)"; context.fillText(text, width/2-textWidth/2, height/2+15); var texture = new THREE.Texture(canvas); texture.needsUpdate = true; var mesh; var material = new THREE.SpriteMaterial({ map: texture, transparent: true, opacity: 0 }); mesh = new THREE.Sprite(material); mesh.scale.set(width/400, height/400, 1); mesh.position.set(0, posY, -3); camera.add(mesh); var tweenIn = new TWEEN.Tween({pos: 0}).to({pos: 1}, fadeTime); tweenIn.onUpdate(function(){ material.opacity = this.pos; }); if(fadeTime === 0) { material.opacity = 1; } else { tweenIn.start(); } var tweenOut = new TWEEN.Tween({pos: 1}).to({pos: 0}, fadeTime); tweenOut.onUpdate(function(){ material.opacity = this.pos; }); tweenOut.onComplete(function(){ camera.remove(mesh); }); tweenOut.fadeOut = tweenOut.start; tweenOut.remove = function(){ camera.remove(mesh); } return tweenOut; };
五、unity地形导出
5.一、首先将unity地形导出为obj
5.二、而后导入3dmax,使用ThreeJSExporter.ms导出为js格式。
六、3dmax动画导出问题
6.一、动画导出错误
一般是对象为可编辑多边形,须要转换成网格对象。
操做步骤:
6.1.一、选择对象,右键转换为可编辑网络;
6.1.二、选择蒙皮修改器,从新蒙皮;
6.1.三、点击蒙皮修改器下的骨骼 > 添加,添加原有的骨骼。
6.二、动画导出错乱
很容易让人觉得是权重出问题了,但就我本身多个项目动画导出的经验来看,大部分出如今骨骼添加上。在3dmax及unity中,不添加根节点每每不影响动画执行,但导出到three.js,须要添加根节点。若是问题还存在,则仔细观察是哪一个骨骼引发的,多余骨骼或缺乏骨骼均可能引发动画错乱。
5、完整的源代码及相应组件
点击下载main.js - 完整的源代码tween.min.js - 动画类OrbitControls.js - 视图控制器,旋转、移动、缩放场景,方便调试audio.min.js - motion音频组件,解决自动播放音频问题其他vr相关组件上文已有介绍