使用three.js渲染3D模型

8,291 阅读4分钟

实际项目中three使用的场景用的也并不多,最多的就是全景项目;最近做了一个博物馆项目,这里整理一下思路

全景项目这里推荐photo-sphere-viewer 它的依赖也是three;

Demo:source.nullno.com/quanjingnew…

理解three:

1.一个可以在某个3D建模软件打开的东西,通过某种方案在浏览器中打开;

2.不要试图手动去创建3D图形,当然比较闲的话可以这样操作,

3.把three当作一个3D模型播放器,在播放器里可以对模型做一些操作:调色调光,调整坐标,切换视角,播放模型中的动画....;

4.某个建模软件(3dMax...)把已经做好的模型导出了模型文件(ojb,glb,gltf,fbx等格式),通过某个可支持此格式的Loader,最终渲染到场景里 ,就像webpack打包css需要css-loader一样的道理;

5.three 把WebGL的图形引擎封装成了3d api  理解WebGL:www.cnblogs.com/wanbo/p/675…

使用three:

基本思路:

构建场景****Scene->通过****Renderer渲染场景到画布(此时看到应是黑屏)->需要借助****Camera帮忙观察物体(此时仍然啥也木有,因为没放物体进去)->通过loader(GLTFLoader,FBXLoader...)加载

模型 ->通过requestAnimationFrame 把最新场景数据渲染出来

你通过屏幕看到永远只是平面,你看到3d物体也是相机观察后的投影,也就是下图编号区域,下方的图片也是另一台相机投影后的结果

{
      Scene: new THREE.Scene(),//场景
      Renderer:new THREE.WebGLRenderer(),//渲染器
      Camera:new THREE.PerspectiveCamera(),//相机
      Controls:new THREE.OrbitControls(this.Camera, this.Renderer.domElement),//控制器
      Model:null,//我的模型
      Lights:null,//设置光源
      AnimationMixer:null,//播放模型动画
}

接下来直接上源码:

演示效果:source.nullno.com/three-demo/

import  *as THREE from './lib.js';

import BG from './model/environment/bg.jpeg'

//模型
import xsr_fbx from './model/xsr/xsr.fbx'

//纹理贴图
import xsr_fbx_texture from './model/xsr/Stormland Robo 03H.png'

import xsr_fbx_logo_texture from './model/xsr/stormland_logo.png'


 const models = [
                {name:'机器头',path:require('./model/DamagedHelmet.glb').default,position:[0, 0, 5],type:'glb'},
                {name:'像素人',path:xsr_fbx,position:[0, 0, 50],type:'fbx',
                texture:[
                         {name:'gardener,hologram_2,hologram',path:xsr_fbx_texture},
                         {name:'Plane',path:xsr_fbx_logo_texture}],
                 }, 
                
               ]

const modelScene={
      State:{
        showGrid:false,
        showLightOrigin:false,
        wireframe:false,
      },
      Scene:null,
      Renderer:null,
      Camera:null,
      Model:null,
      Lights:null,
      AnimationMixer:null,
      Tclock:new THREE.Clock(),
      TestGui:null,
      TestStats:null,
      Controls:null,
      GridHelper:new THREE.GridHelper( 300, 50,  0x00FF12, 0xFFFFFF ),
      
      init:{
          //添加场景
           Scene:function(){
                  this.Scene =  new THREE.Scene()
                  this.Scene.background = new THREE.Color(0x282923);
                  
                  this.Scene.background = new THREE.TextureLoader().load(BG)
         
                  // THREE.Cache.enabled = true;
         
           },
           //添加渲染器
           Renderer:function(){
                  this.Renderer = new THREE.WebGLRenderer({antialias: true,alpha: true,premultipliedAlpha:true,precision: 'highp'})
                  this.Renderer.setPixelRatio(window.devicePixelRatio);
                  this.Renderer.setSize(window.innerWidth, window.innerHeight);
                  this.Renderer.setClearColor(0xeeeeee);
                  this.Renderer.shadowMap.enabled = true;
                  this.Renderer.physicallyCorrectLights = true;
                  this.Renderer.outputEncoding = THREE.sRGBEncoding;


                  ThreeApp.appendChild(this.Renderer.domElement); 
           },
           //添加相机
           Camera:function(){
             this.Camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight,1, 10000)
             this.Camera.position.set(0, 0, 50);
             this.Camera.lookAt(this.Scene.position)
           },

         
           TestGui:function(){
                  let _this = this;
                  this.TestGui =  new THREE.Dat.GUI()
                  this.TestGui.add({
                  changeBg: function () {
                    _this.Scene.background = new THREE.Color(0x1A1A1A);

                  }
                }, "changeBg");
           },
           //帧率状态
           Stats() {
              this.TestStats = new THREE.Stats();
              document.body.appendChild(this.TestStats.dom);
            }
      
      },

      //加载模型GLTF FBX
      modelLoader:function(MODEL){

             
              const loadTip = this.addLoadTip();
             
              this.Controls.autoRotate = false;
                //添加环境hdr
              MODEL.hdr && this.HdrLoader(MODEL.hdr);
             
              let Loader = '',MTYPE = MODEL.type || 'glb';
                 
              if('glb,gltf'.indexOf(MTYPE)!=-1){
                Loader = new THREE.GLTFLoader()
              }
               else if('fbx'.indexOf(MTYPE)!=-1){
                Loader = new THREE.FBXLoader()
              }else{
                 loadTip.textContent='请使用glb,gltf,fbx格式模型';
                 return;
              }
        

   
             

               Loader.load(MODEL.path, (geometry)=> {
                
                  loadTip.textContent='加载完成!';
                  //移除模型  
                  this.Model &&  this.Scene.remove(this.Model);
                
                  //设置相机位置
                  this.Camera.position.set(...MODEL.position);
              
                  this.Model = 'fbx'.indexOf(MTYPE)!=-1?geometry:geometry.scene;
                  
                 
                   //遍历模型字节点,获取相关参数设置
                  this.Model.traverse(function(child) {

                      if(MODEL.texture){
                        
                        MODEL.texture.map(item=>{
                           if(item.name.indexOf(child.name)!=-1){
                               child.material = new THREE.MeshPhongMaterial({
                                         map: THREE.ImageUtils.loadTexture(item.path)//颜色贴图
                                       });
                           }
                        }) 
                      }   
                       

                        if (child.isMesh) {
                            
                              // child.material.emissiveMap = child.material.map;
                           
                              //child.material.side = THREE.DoubleSide;
                              child.material.shininess=1;
                        
                              child.castShadow = true
                              child.receiveShadow = true

                              child.material.transparent=true;//材质允许透明 如果有玻璃材质时开启
                              child.material.opacity=1;//材质默认透明度                        
                            
                          }
                      } 
                   );

                  //模型自动居中
                  THREE.ModelAutoCenter(this.Model)
              
                  //查找模型动画,
                  if(this.Model.animations.length>0){

                       this.AnimationMixer = new THREE.AnimationMixer(this.Model);
                       this.AnimationMixer.clipAction(this.Model.animations[0]).play();

                       /* 其他模型动画方案:
                        const animationClip = this.Model.animations.find(animationClip => animationClip.name === "Walk");
                        this.AnimationMixer.clipAction(animationClip).play();
                        */
                  }


                  //把模型放入场景中
                  this.Scene.add(this.Model);

                  //加载完成后开始自动播放
                  setTimeout(()=>{
                     loadTip.style.display='none';
                     this.Controls.autoRotate = true;
                   },1000);
                  
                },
                (xhr)=>{
                   //加载进度
                   loadTip.textContent=(parseInt(xhr.loaded/xhr.total*100))+'%加载中...';
              
                },
                (err)=>{
                    loadTip.textContent='模型加载失败!'
                    console.log('模型加载失败!')
                }
              );
      },
      //加载光源
      addLight:function(){
            this.Lights = [
                  {name:'AmbientLight',obj:new THREE.AmbientLight(0xFFFFFF,1)},
                  {name:'DirectionalLight_top',obj:new THREE.DirectionalLight(0xFFFFFF,3),position:[0, 15, 0]},
                  {name:'DirectionalLight_bottom',obj:new THREE.DirectionalLight(0x1B1B1B,3),position:[0, -200, 0]},
                  {name:'DirectionalLight_right1',obj:new THREE.DirectionalLight(0xFFFFFF,1.5),position:[0, -5, 20]},
                  {name:'DirectionalLight_right2',obj:new THREE.DirectionalLight(0xFFFFFF,1.5),position:[0, -5, -20]},
            ];
           

           this.Lights.map(item=>{
            item.obj.name=item.name;
            item.position && item.obj.position.set(...item.position);
            item.Helper = new THREE.PointLightHelper( item.obj );
            this.Scene.add(item.obj);
           })  
           

      }, 
      //加载HDR贴图环境光
      HdrLoader:function(HDR){
          const pmremGenerator = new THREE.PMREMGenerator(this.Renderer); // 使用hdr作为背景色
                pmremGenerator.compileEquirectangularShader();
          const textureLoader = new THREE.RGBELoader()
               textureLoader.load(HDR,(texture, textureData)=> {
             
                const envMap = pmremGenerator.fromEquirectangular(texture).texture;

                envMap.isPmremTexture = true;
                pmremGenerator.dispose();
           
                this.Scene.environment = envMap; // 给场景添加环境光效果
                this.Scene.background = envMap; // 给场景添加背景图


              });
      },
      //添加事件
      addControls:function() {
         this.Controls = new THREE.OrbitControls(this.Camera, this.Renderer.domElement);
        // 如果使用animate方法时,将此函数删除
        //controls.addEventListener( 'change', render );
        // 使动画循环使用时阻尼或自转 意思是否有惯性
         this.Controls.enableDamping = true;
        //是否可以缩放
         this.Controls.enableZoom = true;
        //设置相机距离原点的最远距离-可以控制缩放程度
         this.Controls.minDistance = 0;
        //设置相机距离原点的最远距离
         this.Controls.maxDistance = 3000;//800
        //是否开启右键拖拽
         this.Controls.enablePan = false;
        //动态阻尼系数 就是鼠标拖拽旋转灵敏度
         this.Controls.dampingFactor = 0.5;
        //是否自动旋转
         this.Controls.autoRotate = false;
         this.Controls.autoRotateSpeed = 1;
      },
      //模型切换
      switchModel(){
           const _scope = this; 
   
          var switchModelStyle = document.createElement('style');
             switchModelStyle.type = "text/css";
             switchModelStyle.innerText +='.modelList{position:fixed;width:100%; display:flex;justify-content:space-around; bottom:0;left:0;color:#0EF4F4;background:rgba(14,14,44,0.9);cursor:pointer;}\
                .modelList li{width:50%;line-height:30px;padding:5px;text-align:center;font-size:14px;}.modelList li:last-child{border:0;}.modelList li:hover,.modelList li.on{background:#0E2440;}'

          const modelUL = document.createElement('ul');
                modelUL.className='modelList'

                models.map((item,index)=>{
                
                    modelUL.innerHTML+='<li class="'+(index==0?'on':'')+'">'+item.name+'</li>';
                }) 


          document.head.insertBefore(switchModelStyle, document.head.lastChild);
          ThreeApp.insertBefore(modelUL,ThreeApp.firstChild);
       

            let LIS = modelUL.children;
      
              for(let i=0;i<LIS.length;i++){
              
                LIS[i].onclick=function(){
                    for(let j=0; j<LIS.length; j++){
                       LIS[j].className='';
                    }
                     this.className='on'
                     _scope.modelLoader(models[i]);
                     
                  }
              }

      },  
     //添加加载进度
     addLoadTip:function(){
       
            document.querySelector('.loadTip') && ThreeApp.removeChild(document.querySelector('.loadTip'));
           let  loadTip =  document.createElement('div');
                loadTip.className='loadTip'
                loadTip.style.cssText+='position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:5px;background-color:rgba(0,0,0,0.5);padding:5px 10px;color:#fff;';
                ThreeApp.appendChild(loadTip);
              return loadTip;
      },
    
      //添加辅助面板
      addPanel:function(){

            const _scope = this;
            
            //帧率状态
            this.init.Stats.call(this)   
    

             // 添加
            const Panels=[
                  {
                    name:'辅助网格',
                    todo:function(){
                    
                        if(_scope.State.showGrid){
                          _scope.Scene.remove(_scope.GridHelper);
                          _scope.State.showGrid=false;
                             
                        }else{
                           _scope.Scene.add(_scope.GridHelper);
                          _scope.State.showGrid=true;
                        }
                       
                    }
                  },
                  {
                    name:'显示光源',
                    todo:function(){
                       
                            if(_scope.State.showLightOrigin){
                               _scope.Lights.map(item=>{_scope.Scene.remove(item.Helper)})
                              _scope.State.showLightOrigin = false
                            }else{
                              _scope.Lights.map(item=>{_scope.Scene.add(item.Helper)})
                              
                              _scope.State.showLightOrigin = true
                            }
                        
                    }
                  },
                   {
                    name:'骨架模式',
                    todo:function(){
                            if(_scope.State.wireframe){
                                _scope.Model.traverse( child=> {
                                     if ( child.isMesh ) {
                                        child.material.wireframe=false 
                                      }
                                })
                              _scope.State.wireframe = false
                            }else{
                               _scope.Model.traverse( child=> {
                                    if ( child.isMesh ) {
                                      child.material.wireframe=true 
                                    }
                                })
                              _scope.State.wireframe = true
                            }
                        
                    }
                  },

             ]
          
            
            //辅助面板DomTree
            var helpPanelStyle = document.createElement('style');
             helpPanelStyle.type = "text/css";
             helpPanelStyle.innerText +='#helpPanel{position:fixed;width:80px;top:50px;left:0;color:#0EF4F4;background:#0E0E2C;cursor:pointer;}\
                 #helpPanel li{border-bottom:1px solid #fff;line-height:30px;text-align:center;font-size:14px;}#helpPanel li:last-child{border:0;}#helpPanel li.on{color:green;}';
            var helpPanel =  document.createElement('ul');
                helpPanel.id='helpPanel'; 
            

            Panels.forEach(item=>{
               let LI  =  document.createElement('li');
               
                LI.innerText=item.name;
                LI.onclick=function(){
       
                  this.className = this.className=='on'?'':'on'
                  item.todo(this)
                }
                helpPanel.appendChild(LI);
            })
           
         

            document.head.insertBefore(helpPanelStyle, document.head.lastChild);
            ThreeApp.insertBefore(helpPanel,ThreeApp.firstChild)

      },
      animation:function(){

          //更新控制器
            this.Renderer.render(this.Scene, this.Camera);
            this.TestStats.update();
            this.Controls.update();
            this.AnimationMixer && this.AnimationMixer.update(this.Tclock.getDelta());
            requestAnimationFrame(()=>this.animation());
      },
      onWindowResize:function() {
            this.Camera.aspect = window.innerWidth / window.innerHeight;
            this.Camera.updateProjectionMatrix();
            this.Renderer.setSize(window.innerWidth, window.innerHeight);
            this.Renderer.render(this.Scene, this.Camera);
      },
      run:function(){
            this.init.Renderer.call(this) 
            this.init.Scene.call(this)
            this.init.Camera.call(this)

            this.addControls();
             //添加环境光
            this.addLight() 
            this.modelLoader(models[0]);
            
            //添加辅助面板
            this.addPanel();
            this.animation(); 
            this.switchModel()
            
        
            window.onresize = ()=>this.onWindowResize(); 
                 
      }



}


modelScene.run();