随着时间的推移技术的进步,前端越来越杂了,但是也越来越精彩了。只是会用一点ThreeJs,对于WebGl的原理并没了解过,这并不影响我们利用ThreeJs去做出一个非常炫酷的项目。
新世界的大门打开啦!
首先,我们要祭出ThreeJs的最重要的几大组件——scene(场景)、camera(相机)、renderer(渲染器) 、light(灯光),以及渲染的目标——container(就是DOM结构),老生常谈,不多说
打个比方,scene就是舞台,camera就是拍摄舞台的摄像机,它能决定观众看到什么,而一个舞台没有灯光的话它就是黑乎乎的,所以light就是舞台上的各种灯光,所以舞台上表演什么,就是舞台中有什么,所以要加入到scene中 scene.add(“演员们(模型)”)
var camera, scene, renderer; var container; var ambientLight, pointLight; // 初始化 init() // 循环渲染每一帧 一帧一帧的 就是你打游戏时的FPS animate() function init(){ // 初始化相机 // 这里使用的是透视相机来模拟人眼看到的效果 近大远小 camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 ); camera.position.z = 70; camera.position.x = 50; camera.position.y = 10; // 初始化场景 scene = new THREE.Scene(); // 初始化灯光 // 环境光 能保持整体都是亮点 ambientLight = new THREE.AmbientLight(0x404040) // 点光源 就像灯泡一样的效果 白色灯光 亮度0.6 pointLight = new THREE.PointLight(0xffffff, 0.6); // 将灯光加入到场景中 scene.add(ambientLight) // 将灯光加到摄像机中 点光源跟随摄像机移动 // 为什么这样做 因为这样可以让后期处理时的辉光效果更漂亮 camera.add(pointLight); // 我们将摄像机加入到场景中 scene.add(camera); // 初始化渲染器 renderer = new THREE.WebGLRenderer({ // 开启抗锯齿 antialias: true, // 开启背景透明 alpha: true }); // 把自动清除颜色缓存关闭 这个如果不关闭 后期处理这块会不能有效显示 // 书上的描述是 如果不这样做,每次调用效果组合器的render()函数时,之前渲染的场景会被清理掉。通过这种方法,我们只会在render循环开始时,把所有东西清理一遍。 renderer.autoClear = false; // 背景透明 配合 alpha renderer.setClearColor(0xffffff, 0); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // 伽马值启动 更像人眼观察的场景 renderer.gammaInput = true; renderer.gammaOutput = true; // 渲染到DOM中去 container = document.createElement("div"); container.appendChild(renderer.domElement); document.body.appendChild(container); } // 这样一来,基础场景创建就完成了,接下来我们来让它循环渲染起来 function animate() { // 这个方法低版本浏览器兼容不好 可以从github上找些兼容库 如果要兼容低版本浏览器 requestAnimationFrame(animate); // 渲染我们的场景 摄像机啪啪啪的拍和录 // 由于把renderer autoClear 关闭了 所以我们要在渲染函数中手动清除 renderer.clear(); renderer.render(scene, camera); } // ok 基础部分完成 接下来我们来加载模型限于经验和技术等各种外力因素影响,项目最开始时编写demo使用的是Obj模型和Mtl贴图文件(不太确定贴图文件的叫法是否准确),使用起来也很简单(ThreeJs仓库里的webgl_loader_obj_mtl.html拿来改下就行了)
<!DOCTYPE html> <html lang="en"> <head> <title>Threejs-city-model-show</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" /> <style> body { color: #fff; margin: 0px; overflow: hidden; } </style> </head> <body> <script src="../build/three.min.js"></script> <!-- 引入我们可爱的加载器 --> <script src="js/loaders/MTLLoader.js"></script> <script src="js/loaders/OBJLoader.js"></script> <script> /* 省略创建场景部分的代码 */ // 加载的过程 var onProgress = function(xhr) { if (xhr.lengthComputable) { var percentComplete = (xhr.loaded / xhr.total) * 100; console.log(Math.round(percentComplete, 2) + "% downloaded"); } }; var onError = function() { // 载入出错时候 }; // 加载Mtl贴图文件 new THREE.MTLLoader() // 贴图文件的路径 .setPath("models/obj/male02/") .load("male02_dds.mtl", function(materials) { // 看代码意思是预加载 materials.preload(); // 加载OBJ模型 new THREE.OBJLoader() // 设置OBJ模型的材质贴图 .setMaterials(materials) .setPath("models/obj/male02/") .load( "male02.obj", function(object) { object.position.y = -95; scene.add(object); }, onProgress, onError ); }); </script> </body> </html>这一步一般会出现的问题有如下
模型加载后,不显示也不报错? 检查场景是否正常渲染了,如果正常渲染模型的位置在哪里,摄像机在哪里,摄像机是否对着模型,灯光是否配置,模型是否太大或者太小了,超出了摄像机的摄影范围……模型可以正常加载,但是贴图不显示? 首先检查network是否报404错误,如果报错,一般都是mtl贴图文件(看起来像是雪碧图那种)没给你,或者路径配置的不是相对路径,如果贴图没错误,模型是黑色的,在mtl文件中可以更改ka或kd的三个值(对应rgb),或者打印出模型属性,在material.color中更改点色值或别的属性。黑色的时候,看不到贴图。一般这样一通操作之后,就能看到了模型了模型文件太大了,浏览器在渲染的时候进程被完全卡死!要等待几十秒之久!天呐! 这个问题看起来比较棘手,其实很好解决。ThreeJs官方推荐gltf格式的模型在浏览器中渲染,因为它是为浏览器而生的,性能好,体积小。我们项目中使用的模型文件,一开始是Obj和Mtl的,达到25MB大小,在vue项目中渲染会阻塞浏览器46s,原生html+js的项目中好些,几秒时间就行了,我怀疑是我写法的问题,但是我测试仅仅是加载模型渲染到场景,并没有多余操作和数据绑定,还是一样,阻塞进程,一度导致我怀疑人生???黑人问号脸。那么如何将Obj模型转换为gltf模型,还能再优化吗?进入下一章节!对了对了,Obj模型也是可以压缩的,而且ObjLoader2加载会快一点真的很牛逼 模型加贴图从 25mb 减小到了1.8mb 上效果图
1.这是不加贴图和mtl的obj文件 已经达到了22.5MB!
这是obj转gltf之后的文件,贴图转成了base64包含在了gltf文件中,可通过配置项提取出文件,稍后介绍
这是经过gltf压缩处理之后的贴图+模型的文件大小
obj2gltf-github
用法 // 全局安装后 obj文件所在目录 输出目录 obj2gltf -i ./examples/models/obj/hanchuan/city.obj -o ./gltf/city.gltf --unlit --separate 介绍下为什么要加这两个参数 --unlit的作用是可以保留环境贴图的效果,环境贴图后面再介绍 --separate是将贴图文件提取出来,提出来浏览器可以缓存,如果你需要继续压缩gltf文件,这里不加这个参数也行,因为压缩的时候也能提出来gltf-pipeline-github
用法 gltf-pipeline -i ../../../gltf/city.gltf -o ../../../examples/models/obj/hanchuan/city_small1.gltf -d --separate 介绍下参数 -d是--draco.compressMeshes的缩写,使用draco算法压缩模型 --separate就是将贴图文件提取出来,不提可以不加这样,我们就完成了gltf模型的转化和压缩,性能暴增!秒开! 在我们最终的模型中,obj模型297Mb,转gltf之后还有150Mb左右,最终经过压缩,还有7.3Mb!
抛弃了Obj和Mtl之后,我们的加载器也要做一下改变
<!DOCTYPE html> <html lang="en"> <head> <title>Threejs-city-model-show</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" /> <style> body { color: #fff; margin: 0px; overflow: hidden; } </style> </head> <body> <script src="../build/three.min.js"></script> <!-- 引入我们可爱的加载器 --> <script src="js/loaders/GLTFLoader.js"></script> <script src="js/loaders/DRACOLoader.js"></script> <script> /* 省略创建场景部分的代码 */ // 加载的过程 var onProgress = function(xhr) { if (xhr.lengthComputable) { var percentComplete = (xhr.loaded / xhr.total) * 100; console.log(Math.round(percentComplete, 2) + "% downloaded"); } }; var onError = function() { // 载入出错时候 }; var loader = new THREE.GLTFLoader(); // 这个是Threejs解析draco压缩之后的解析器 // 它从这里读取解析器JS THREE.DRACOLoader.setDecoderPath("js/libs/draco/gltf/"); // 将Draco解析器和GltfLoader绑定在一起 loader.setDRACOLoader(new THREE.DRACOLoader()); loader.load( "models/obj/hanchuan/city_small1.gltf", function(gltf) { // gltf.scene 拿到这个可以处理模型 scene.add(gltf.scene) }, onProgress, onError ); </script> </body> </html>这时候的场景,应该是这样的,很丑吧哈哈哈,没关系没关系,我们可以为它美容,不过在此之前,我们先来试着转动这个模型,看看性能怎么样。
它的常用参数在源码中可以找到,也可以百度/goggle一下中文翻译的,不做太多介绍,这是其中一段源码。
// Set to false to disable this control this.enabled = true; // "target" sets the location of focus, where the object orbits around this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) this.minDistance = 0; this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) this.minZoom = 0; this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. // Range is 0 to Math.PI radians. this.minPolarAngle = 0; // radians this.maxPolarAngle = Math.PI; // radians // How far you can orbit horizontally, upper and lower limits. // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. this.minAzimuthAngle = - Infinity; // radians this.maxAzimuthAngle = Infinity; // radians // Set to true to enable damping (inertia) // If damping is enabled, you must call controls.update() in your animation loop this.enableDamping = false; this.dampingFactor = 0.25; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. // Set to false to disable zooming this.enableZoom = true; this.zoomSpeed = 1.0; // Set to false to disable rotating this.enableRotate = true; this.rotateSpeed = 1.0; // Set to false to disable panning this.enablePan = true; this.panSpeed = 1.0; this.screenSpacePanning = false; // if true, pan in screen-space this.keyPanSpeed = 7.0; // pixels moved per arrow key push // Set to true to automatically rotate around the target // If auto-rotate is enabled, you must call controls.update() in your animation loop this.autoRotate = false; this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 // Set to false to disable use of the keys this.enableKeys = true; // The four arrow keys this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; // Mouse buttons this.mouseButtons = { LEFT: THREE.MOUSE.LEFT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: THREE.MOUSE.RIGHT }; // for reset this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); this.zoom0 = this.object.zoom; // // public methods // this.getPolarAngle = function () { 初始化这个控制器之后,就可以操作模型旋转放大缩小了。它的原理就是控制摄像机和模型的距离,同理也可以控制模型与摄像机的距离去实现移动放大缩小等功能,可以自己尝试一下。一个比较有趣的操作是在function animate(){}中,设置camera.lookAt=scene.position效果也很不错。ThreeJs中内置了很多有趣的控制器,用法和效果都可以从ThreeJs的examples中找到,记得看看。玩过LOL,大型单机游戏的同学都知道,如果帧率不好,画面看起来就会卡顿,影响体验,这也为什么用requestAnimationFrame去作为渲染调用的原因之一,它的性能比函数递归和setInterval实现渲染调用好很多。那么我们如何去检测我们的场景渲染的性能怎么样呢?就可以使用Stats
// <script src="js/libs/stats.min.js"></script> 不要忘了引入进来 var stats; function init(){ // 省略创建场景部分 stats = new Stats(); container.appendChild(stats.dom); } function animatie(){ stats.update(); // 省略renderer } 初始化之后在页面左上角会看到,这个原理还没研究过,有机会翻翻源码看看。如果实在vue/react等单页面环境中,可以通过process.env.NODE_ENV控制开发环境再显示这个。这样一来,我们在开发调试的时候,就能很直观的看出效果了。若不为空,在渲染场景的时候将设置背景,且背景总是首先被渲染的。 可以设置一个用于的“clear”的Color(颜色)、一个覆盖canvas的Texture(纹理),或是一个CubeTexture。默认值为null。
实验结果是,TextureLoader、CubeTexture和SphereGeometry都可以作为背景图,简单介绍下这三者。 TextureLoader 一张图,背景看起来是静止不动的CubeTexture 立方纹理 图片是分割成6块 相当于摄像机和模型在一个正方体盒子中 背景随着摄像机转动而转动SphereGeometry 一张图 全景图原理 相当于摄像机和模型在一个圆球盒子中 背景随着摄像机转动而转动不太理解可以百度下threejs全景图原理,不做过多叙述 function init(){ // 省略其余代码 // .... // 添加一张静止的背景图 scene.background = new THREE.TextureLoader().load("你的背景图") // .... } 之后效果大概是这样的,我们的世界里有了天空,其实这里用CubeTexture或者SphereGeometry效果更好细心的同学会发现,河流和楼上会有星星点点的光,这是怎么实现的呢?答案就是环境贴图。
环境贴图 简单的讲,环境贴图就像把物体的表面化作一面镜子,可以反射出你为它赋予的图片。
如何设置环境贴图呢?回到我们加载模型的部分。核心就是创建立方纹理然后设置某个模型的material的envMap为这个立方纹理。 环境贴图的使用限制受纹理影响,有一部分纹理加不上环境贴图。
<!DOCTYPE html> <html lang="en"> <head> <title>Threejs-city-model-show</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" /> <style> body { color: #fff; margin: 0px; overflow: hidden; } </style> </head> <body> <script src="../build/three.min.js"></script> <!-- 引入我们可爱的加载器 --> <script src="js/loaders/GLTFLoader.js"></script> <script src="js/loaders/DRACOLoader.js"></script> <script> /* 省略创建场景部分的代码 */ // 创建一个立方纹理 var envMap = new THREE.CubeTextureLoader() .setPath("textures/") .load(new Array(6).fill("start.jpg")); var loader = new THREE.GLTFLoader(); // 这个是Threejs解析draco压缩之后的解析器 // 它从这里读取解析器JS THREE.DRACOLoader.setDecoderPath("js/libs/draco/gltf/"); // 将Draco解析器和GltfLoader绑定在一起 loader.setDRACOLoader(new THREE.DRACOLoader()); loader.load( "models/obj/hanchuan/city_small1.gltf", function(gltf) { // gltf.scene 拿到这个可以处理模型 gltf.scene.traverse(function(child) { if (child.isMesh) { /* 这些都是DEMO 具体看你模型调整 下节介绍通过鼠标点击确定模型所属对象 然后去调试模型 */ // 这些名称都可以通过打印看出 console.log(child) // 比如我想给这些加上环境贴图 就可以这样写 /hai|city|liubianxing/i.test(child.name) && (child.material.envMap = envMap); if (/city/i.test(child.name)) { // 更改模型颜色 child.material.color = new THREE.Color(6, 6, 5); // 更改模型环境贴图影响 0-1 child.material.reflectivity = 0.9; } // 更改模型位置 /lumian|hai/i.test(child.name) && (child.position.y = 0.5); // ... } }); scene.add(gltf.scene) }, onProgress, onError ); </script> </body> </html>光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
打印出所有的child不好定位是哪块模型,有没有更快的方法?您好,有的。通过 THREE.Raycaster 实现模型选中与信息显示,点击打印出当前点击的模型,在它的属性中修改颜色,位置等,可以直接更新效果,调试更方便到此,经过我们的美化之后,效果就是这样了。还缺了点什么,道路咋不发光啊,看着没光效,不炫酷!这一块的基础建议好好看看《THREE.JS开发指南》这本书。如果需要多个pass,要学会使用MaskPass和clearPass。这一块因为不熟悉,我在添加效果的时候花费了很大量的时间,尤其是Threejs内置的pass效果没有文档,甚至你都不知道内置了多少种效果…《THREE.JS开发指南》这本书介绍的比较全面,用法也很详细。
这样 辉光效果就出来了。还不够还不够,让我们加上FocusShaper,让它看起来像聚焦在中心一样(突出中心)。
颜色越亮,发光效果越强辉光受环境贴图影响模型可以通过map贴图来更改亮度,比如暗色的贴图,它反光就会很软我们要引入FocusShader。
FocusShader是一个简单的着色器,其结果是中央区域渲染的比较锐利,单周围比较模糊。 <!DOCTYPE html> <html lang="en"> <head> <title>Threejs-city-model-show</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" /> <style> body { color: #fff; margin: 0px; overflow: hidden; } </style> </head> <body> <!-- 省略其他引入的 --> <!-- 引入Effect --> <script src="js/postprocessing/EffectComposer.js"></script> <!-- 引入Effect配套的render --> <script src="js/postprocessing/RenderPass.js"></script> <script src="js/postprocessing/ShaderPass.js"></script> <!-- 引入各种需要的shader --> <script src="js/shaders/CopyShader.js"></script> <script src="js/shaders/LuminosityHighPassShader.js"></script> <script src="js/postprocessing/UnrealBloomPass.js"></script> <!-- focusShader 相对于bloompass新加的 --> <script src="js/shaders/FocusShader.js"></script> <script> var clock; /* 省略创建场景部分的代码 */ // 创建focusShader 相对于bloompass新加的 var focusShader = new THREE.ShaderPass(THREE.FocusShader); focusShader.uniforms["screenWidth"].value = window.innerWidth; focusShader.uniforms["screenHeight"].value = window.innerHeight; focusShader.uniforms["sampleDistance"].value = 1.07; // 初始化renderPass var renderScene = new THREE.RenderPass(scene, camera); // 初始化bloomPass var bloomPass = new THREE.UnrealBloomPass( // 没研究过这些参数的意义 会提上日程 new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85 ); // 一些参数 可以调整看效果 bloomPass.threshold = 0.36; bloomPass.strength = 0.6; bloomPass.radius = 0; // effectCopy var effectCopy = new THREE.ShaderPass(THREE.CopyShader); // 让effectCopy渲染到屏幕上 没这句不会再屏幕上渲染 effectCopy.renderToScreen = true; // 初始化 composer var composer = new THREE.EffectComposer(renderer); // 模版缓冲(stencil buffer) https://blog.csdn.net/silangquan/article/details/46608915 composer.renderTarget1.stencilBuffer = true; composer.renderTarget2.stencilBuffer = true; composer.setSize(window.innerWidth, window.innerHeight); composer.addPass(renderScene); composer.addPass(bloomPass); // 相对于bloompass新加的 composer.addPass(focusShader); composer.addPass(effectCopy); // 修改animate function animate() { requestAnimationFrame(animate); var delt = clock.getDelta(); stats.update(); renderer.clear(); // 删除renderer使用composerrender去渲染 // renderer.render(scene, camera); // 没理解透这个delt的作用 ??? composer.render(delt); } </script> </body> </html>模型的渲染和后期处理就到此就全部结束了。
精灵是一个总是面朝着摄像机的平面,通常含有使用一个半透明的纹理。
var textured = new THREE.TextureLoader().load("textures/warning.png"); var spriteMaterial = new THREE.SpriteMaterial({ // color: 0xffffff, map: textured }); var sprite = new THREE.Sprite(spriteMaterial); sprite.position.set( 25.729931791092394, 10.179400757773436, 36.07142388020101 ); // console.log(sprite); sprite.scale.x = 10; sprite.scale.y = 5; scene.add(sprite);这张图火灾预警的图其实就是一张透明的png图片,精灵可以用canvas贴图,你可以自己编写canvas渲染在指定点上,也可以使用CSS3DRenderer去实现。
通常的情况下Threejs里的模型是要分组的。在处理交互起来,有分组会更加清晰明了,就像模块拆分一样。
var group = new THREE.Group();由于单页面中,Threejs创建的任何材质,模型,贴图……只要含有dispose方法的,你在页面组件即将销毁的周期中,都要调用下dispose方法清除,不然可能内存泄漏。刚学会一个妙招,利用WEBGL_lose_context这个API 可以让当前的webgl环境失效,达到取消占用的目的。
beforeDestory(){ this.bloomPass.dispose(); this.envMap.dispose(); this.skymap.dispose(); this.dracoLoader.dispose(); this.spriteMaterial.dispose(); this.sphereGeometry.dispose(); this.meshBasicMaterial.dispose(); this.scene.dispose(); this.controls.dispose(); /* const data = this.$data; for (let i in data) { if (data.hasOwnProperty(i)) { if (data[i] && typeof data[i].dispose == "function") { data[i].dispose(); } } } */ // this.renderer.domElement 就是你的threejs的canvas Dom let gl = this.renderer.domElement.getContext("webgl"); gl && gl.getExtension("WEBGL_lose_context").loseContext(); }使用SSAA、FXAA、SMAA等抗锯齿后处理。任选其一即可。
initFxaaPass() { let fxaaPass = new ShaderPass(FXAAShader); const pixelRatio = this.renderer.getPixelRatio(); fxaaPass.material.uniforms["resolution"].value.x = 1 / (this.width * pixelRatio); fxaaPass.material.uniforms["resolution"].value.y = 1 / (this.height * pixelRatio); fxaaPass.renderToScreen = true; this.fxaaPass= fxaaPass; }, initSmaaShader() { const pixelRatio = this.renderer.getPixelRatio(); this.smaaPass = new SMAAPass( this.width * pixelRatio, this.height * pixelRatio ); this.smaaShader.renderToScreen = true; }, initSsaaShader() { this.ssaaRenderPass = new SSAARenderPass(this.scene, this.camera); this.ssaaRenderPass.unbiased = false; this.ssaaRenderPass.sampleLevel = 2; },利用EffectComposer应用某个效果
initEffectComposer() { const composer = new EffectComposer(this.renderer); composer.setSize(this.width, this.height); composer.addPass(this.renderScene); composer.addPass(this.ssaaRenderPass); composer.addPass(this.bloomPass); composer.addPass(this.focusShader); composer.addPass(this.effectCopy); this.composer = composer; },
