精华内容
下载资源
问答
  • Three.js实现3D机房效果

    2020-12-11 11:39:24
    3D机房系统是最近用户的需求,通过相关了解最后使用Three.js,也发现最近有东西可以写出来分享: webGL可以让我们在canvas上实现3D效果。而three.js是一款webGL框架,由于其易用性被广泛应用 Three.js是通过对WebGL...
  • 使用ThreeJS实现的3D机房效果,包含简单的设备增删改查功能、设备告警功能、机柜拖动调整位置功能等
  • ThreeJS —— 机房Demo(一)

    千次阅读 2020-06-05 16:25:32
    ThreeJS —— 机房Demo(一)初始化Three 最近对3D可视化这一块比较感兴趣,通过了解ThreeJS是用来实现3D可视化的一种常用方法,于是在自学ThreeJS的基础上,打算写一个机房Demo来练手 初始化Three 引入必需的js文件...

    最近对3D可视化这一块比较感兴趣,通过了解ThreeJS是用来实现3D可视化的一种常用方法,于是在自学ThreeJS的基础上,打算写一个机房Demo来练手

    点这里预览项目
    GitHub

    目录结构

    ├── font // 字体文件
     |├──── font.ttf // 字体源文件
     |└──── font.json // 转换后的字体文件
    ├── img // 素材图片
     |├──── xx.png
     |├──── xxx.jpg
     |└──── …
    ├── js // 自己编写的js文件
     |├──── composer_fn.js // 后期处理
     |├──── create_fn.js // 创建各种几何
     |├──── init_fn.js // 初始化项目
     |└──── util_fn.js // 工具函数
    ├── lib // 需要引入的js文件
     |├──── three.js
     |├──── OrbitControls.js
     |├──── RenderPass.js
     |└──── …
    ├── model // 建模工具导出的模型
     |├──── computer.gltf
     |└──── …
    └── index.html // 入口文件

    初始化Three三大件:场景、相机、渲染器

    首先我们应该对Three进行初始化,准备好我们的相机和渲染器,搭建好场景

    • 初始化场景
    const scene = new THREE.Scene();
    // 设置场景背景图,三种类型:
    // 1. 普通背景图,一个平面
    scene.background = new THREE.Color("rgb(25, 35, 39)");
    scene.background = new THREE.TextureLoader().load('img/back.jpg');
    
    // 2. 立方体背景图
    scene.background = new THREE.CubeTextureLoader().setPath('img/').load(new Array(6).fill('back.jpg'));
    
    // 3. 球型全景(背景)图,通过建立球体,并反向放大100倍实现,其中x放大倍数为负数
    const geometry = new THREE.SphereGeometry(5, 32, 32);
    const material = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load("img/back.jpg") });
    const sphere = new THREE.Mesh(geometry, material);
    scene.add(sphere);
    geometry.scale(- 100, 100, 100);
    
    • 初始化相机
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.set(-20, 40, 90); // 设置相机的初始位置
    
    • 初始化渲染器
    const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); // alpha:背景透明,antialias:抗锯齿
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement); // 加入body中,也可以加入任意元素里
    
    • 最终在 init_fn.js 中合并成一个函数 initThree
    // init_fn.js
    function initThree(selector) {
      const scene = new THREE.Scene();
      scene.background = new THREE.Color("rgb(25, 35, 39)");
      
      const camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      camera.position.set(-20, 40, 90);
    
      const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.querySelector(selector).appendChild(renderer.domElement);
      
      return { scene, camera, renderer };
    }
    

    添加轨道控制器

    为了增加用户交互性,我们需要添加控制器,添加后就可以通过滚轮缩放控制模型大小,鼠标左键旋转,鼠标右键平移

    • 首先在 index.html 中引入所需文件
    <!-- index.html -->
    <script src="lib/OrbitControls.js"></script>
    
    • 然后创建轨道控制器
    // init_fn.js
    function initControls() {
      const controls = new THREE.OrbitControls(camera, renderer.domElement);
      controls.addEventListener('change', function () { ... }); // 添加事件
      return controls;
    }
    

    添加Stats

    Stats能实时监听fps的变化,用来监测渲染场景的性能

    • 首先在 index.html 中引入所需文件
    <!-- index.html -->
    <script src="lib/stats.min.js"></script>
    
    • 然后创建Stats
    // init_fn.js
    function initStats() {
      const stats = new Stats();
      document.body.appendChild(stats.dom);
      return stats;
    }
    

    初始化灯光

    灯光用来给物体上色,没有灯光的物体将一片漆黑,我们通常先加入一个自然光,确保每个物体都能呈现出来,然后再根据需求添加任意灯光

    // init_fn.js
    function initLight() {
      const ambientLight = new THREE.AmbientLight(0xffffff); // 自然光,每个几何体的每个面都有光
      const pointLight = new THREE.PointLight(0xff0000, 10); // 点光源
      pointLight.position.set(0, 50, 0); // 调整点光源位置
      scene.add(ambientLight);
      scene.add(pointLight);
      return [ambientLight, pointLight];
    }
    

    编写入口文件

    编写 index.html,引入前面编写好的 init_fn.js 函数来初始化Three

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>Three-Demo</title>
        <style type="text/css">
          body {
            margin: 0;
          }
    
          #canvas-frame {
            border: none;
            background-color: #eeeeee; /* 设置背景颜色 */
          }
        </style>
      </head>
    
      <body>
        <div id="canvas-frame"></div>
        
    	<!-- 引入ThreeJS -->
        <script src="lib/three.js"></script>
        <!-- 引入控制器 -->
        <script src="lib/OrbitControls.js"></script>
        <!-- 引入fps -->
        <script src="lib/stats.min.js"></script>
        <!-- 自己封装好的函数 -->
        <script src="js/init_fn.js"></script>
        
        <script>
          // 初始化
          const { scene, camera, renderer } = initThree(
            "#canvas-frame"
          );
          const lights = initLight();
          const controls = initControls();
          const stats = initStats();
          // camera.lookAt(10, 10, 10);
    
    	  function animate(time) {
            stats.update(); // 初始化stats后,需要在这里执行update方法才能实现fps实时监控
            renderer.render(scene, camera); // 最后需要将场景渲染出来,没有这句将什么都显示不了
            requestAnimationFrame(animate); // 这里利用浏览器API——requestAnimationFrame,每帧都进行渲染,执行renderer.render(...)方法
          }
          animate();
        </script>
      </body>
    </html>
    

    创建一台机器(一)—— 创建几何体

    上面的步骤仅仅只是对Three的初始化,也就是前期准备,此时场景里还是空空如也,什么都没有,我们需要往场景里添加各种几何体并渲染出来,下面我们将添加我们的第一个几何体

    效果图:
    机器一
    通过创建一个球体,并进行贴图,模拟网络中的一台机器

    // create_fn.js
    // 创建一台机器(球体)
    function createEarth() {
      const geometry = new THREE.SphereBufferGeometry(5, 64, 64); // 构建一个球型几何体,BufferGeometry性能比Geometry好
      const texture = new THREE.TextureLoader().load("./img/earth.png"); // 创建一个纹理贴图,将其贴到一个表面
      const material = new THREE.MeshBasicMaterial({ map: texture }); // 创建一个材质,map属性传入刚刚创建好的纹理贴图
      const mesh = new THREE.Mesh(geometry, material); // 利用Mesh将几何体和材质联系在一起,形成最终的物体
      mesh.position.x = -15; 
      mesh.position.y = -1; // 修改几何体的位置
      return mesh;
    }
    

    将几何体添加至场景中,然后渲染

    <!DOCTYPE html>
    <html>
      <head>...</head>
    
      <body>
        <div id="canvas-frame"></div>
        
    	<!-- 引入的一些JS -->
        <script src="lib/three.js"></script>
        <script src="lib/OrbitControls.js"></script>
        <script src="lib/stats.min.js"></script>
        
        <!-- 自己封装好的函数 -->
        <script src="js/init_fn.js"></script>
        <script src="js/create_fn.js"></script>
        
        <script>
          const { scene, camera, renderer } = initThree(
            "#canvas-frame"
          );
          const lights = initLight();
          const controls = initControls();
          const stats = initStats();
          
          // 新添加的代码
          const mesh = createEarth(); 
          scene.add(mesh); // 将物体添加到场景中
    
          function animate(time) {
             stats.update();
             renderer.render(scene, camera);
             requestAnimationFrame(animate);
           }
           animate();
        </script>
      </body>
    </html>
    

    创建一台机器(二)—— 创建多材质几何体

    效果图:
    机器二
    通过创建一个圆柱体,并对不同面进行贴图(上下底面用贴图,侧面用纯色),模拟一台机器

    // create_fn.js
    // 创建一台机器(圆柱),path为上下底面的贴图图片路径
    function createMachine(path, conf) {
      const geometry = new THREE.CylinderBufferGeometry(5, 5, 2, 64); 
      const texture = createTexture(path); // 因为经常要用到贴图,所以摘出一个函数来创建纹理贴图
      const bottomMaterial = new THREE.MeshBasicMaterial({ map: texture });
      const sideMaterial = new THREE.MeshBasicMaterial({ color: "#1296DB" });
      const materials = [sideMaterial, bottomMaterial, bottomMaterial]; /* 材质material可以为一个值,也可以为一个数组,若是数组则表示对每个面应用不同的材质 
      这里用数组,第一个元素是侧面的材质,第二个元素是上面那个面的材质,第三个元素是下面那个面的材质 */
      const mesh = new THREE.Mesh(geometry, materials);
      initConfig(mesh, conf); // 因为经常要对物体进行变形(改变位置、大小等),所以单独写一个函数
      return mesh;
    }
    
    // 创建一种纹理贴图,path为贴图图片路径
    function createTexture(path, conf) {
      const texture = new THREE.TextureLoader().load(path);
      initConfig(texture, conf);
      return texture;
    }
    
    // 对传入的conf进行处理,因为大部分几何体都能对其position(位置)、rotation(渲染)、scale(缩放)等进行设置
    // 应用举例:initConfig(mesh, { position: { x: -15, y: -1 } })
    // 第一个参数不一定要传入mesh,也可以传入纹理Texture 
    function initConfig(mesh, conf) {
      if (conf) {
        const { position, rotation, scale, repeat } = conf;
        if (position) {
          const { x, y, z } = position;
          x ? (mesh.position.x = x) : null;
          y ? (mesh.position.y = y) : null;
          z ? (mesh.position.z = z) : null;
        }
        if (rotation) {
          const { x, y, z } = rotation;
          x ? (mesh.rotation.x = x) : null;
          y ? (mesh.rotation.y = y) : null;
          z ? (mesh.rotation.z = z) : null;
        }
        if (scale) {
          const { x, y, z } = scale;
          x ? (mesh.scale.x = x) : null;
          y ? (mesh.scale.y = y) : null;
          z ? (mesh.scale.z = z) : null;
        }
        if (repeat) {
          const { x, y } = repeat;
          // 对Texture的repeat进行处理
          if (x) {
            // 设置x方向的重复数
            mesh.wrapS = THREE.RepeatWrapping;
            mesh.repeat.x = x;
          }
          if (y) {
            // 设置y方向的重复数
            mesh.wrapT = THREE.RepeatWrapping;
            mesh.repeat.y = y;
          }
        }
      }
    }
    

    创建一台机器(三)—— 导入模型

    通常在实际项目中,仅仅靠ThreeJS自带的一些几何体创建出来的物体满足不了我们的需求,这个时候就要利用3DS MAX、blender等建模软件建模,然后在Three中引入

    • 引入需要的加载器文件
    <!-- index.html -->
    <!-- 导入gltf模型,需要这两个加载器 -->
    <script src="lib/DRACOLoader.js"></script>
    <script src="lib/GLTFLoader.js"></script>
    
    • 导入模型
    // create_fn.js
    // 创建一个导入的模型,path为模型路径
    function createImportModel(path, conf) {
      // 因为GLTFLoader只能用回调函数的形式获取到几何体,所以加入Promise方便我们后面的获取
      return new Promise((res) => {
        const dracoLoader = new THREE.DRACOLoader().setDecoderPath("../js/draco/"); // ThreeJS源码中有一个example文件夹,其中js目录下有一个draco目录,同样要将这个目录引入进来
        const loader = new THREE.GLTFLoader().setDRACOLoader(dracoLoader);
        loader.load(path, function (gltf) {
          // gltf对象中有很多属性,gltf.scene就是我们需要的几何体
          initConfig(gltf.scene, conf);
          res(gltf.scene); // 将物体传出去
        });
      });
    }
    

    效果图:
    机器三初始
    通常引入的模型是由多个几何体组成的,这个时候组成模型的每个几何体都没有对应的材质,需要我们手动为每一个几何体添加材质

    // create_fn.js
    // 创建一个导入的模型
    function createImportModel(path, conf) {
      return new Promise((res) => {
        const dracoLoader = new THREE.DRACOLoader().setDecoderPath("../js/draco/");
        const loader = new THREE.GLTFLoader().setDRACOLoader(dracoLoader);
        loader.load(path, function (gltf) {
          const colorArr = [
            "#999",
            "rgb(110, 105, 112)",
            "#7fffd4",
            "#ffe4c4",
            "#faebd7",
            "#a9a9a9",
            "#5f9ea0",
            "#6495ed",
          ];
          // scene中有一个traverse方法,可以遍历其子元素,然后判断该子元素是不是属于Mesh类,如果是则表示该子元素是一个几何体,可以对其执行相应操作:添加材质
          gltf.scene.traverse(function (child) {
            if (child instanceof THREE.Mesh) {
              // 为该模型的不同部件(即不同几何体)添加不同颜色材质(上色)
              child.material = new THREE.MeshBasicMaterial({
                color: colorArr.pop(),
              });
            }
          });
          initConfig(gltf.scene, conf);
          res(gltf.scene); // 将物体传出去
        });
      });
    }
    

    新的模型:
    机器三(加入线框前)

    添加线框效果

    不过这样看还是感觉少了什么……对!面与面之间没有明显的边界,融为了一体。所以我们需要进一步优化这个模型:

    // create_fn.js
    // 创建一个导入的模型
    function createImportModel(path, conf) {
      return new Promise((res) => {
        const dracoLoader = new THREE.DRACOLoader().setDecoderPath("../js/draco/");
        const loader = new THREE.GLTFLoader().setDRACOLoader(dracoLoader);
        loader.load(path, function (gltf) {
          const colorArr = [
            "#999",
            "rgb(110, 105, 112)",
            "#7fffd4",
            "#ffe4c4",
            "#faebd7",
            "#a9a9a9",
            "#5f9ea0",
            "#6495ed",
          ];
          gltf.scene.traverse(function (child) {
            if (child instanceof THREE.Mesh) {
              child.material = new THREE.MeshBasicMaterial({ color: colorArr.pop() });
              
              // 为该模型的不同部件(即不同几何体)添加线框,使每个部分棱角分明,显得更逼真
              const geometry = new THREE.EdgesGeometry(child.geometry); // 边缘几何体
              const material = new THREE.LineBasicMaterial({ color: "#dcdcdc" }); // 线框材质
              // material.depthTest = false; // 深度测试,若开启则是边框透明的效果
              const mesh = new THREE.LineSegments(geometry, material);
              child.add(mesh); // 必须在child(即该部件)中加入,不能在scene中加入,以确保和几何体的相对位置始终保持一致
            }
          });
          initConfig(gltf.scene, conf);
          res(gltf.scene); // 将物体传出去
        });
      });
    }
    

    到这一步,我们该做的工作就都做完了,大功告成!最终效果图:
    在这里插入图片描述

    融合材质

    Tips:最后增加边界的工作,实际上是利用两个模型放置在同一个位置实现的效果,还有一种类似实现方式—— 融合材质

    写在后面的话

    这是本人第一次研究3D可视化,学习ThreeJS以及WebGL,如有纰漏或疑问,还望在评论区指出

    本文所参考的文章:

    展开全文
  • 作者:Gvonte,关键技术点:效果合成器EffectComposer实现部分辉光效果,加入抗锯齿,高级效果组合器 MaskPass。
  • ThreeJS —— 机房Demo(四)

    千次阅读 2020-06-06 22:14:01
    ThreeJS —— 机房Demo(四)目录结构创建辉光效果效果合成器 EffectComposer部分辉光效果加入抗锯齿高级效果组合器 MaskPass 上一节我们提到了光圈效果,除了这种光效,还有一个光效是3D可视化常用的,那就是辉光...

    上一节我们提到了光圈效果,除了这种光效,还有一个光效是3D可视化常用的,那就是辉光效果

    目录结构

    ├── font // 字体文件
     |├──── font.ttf // 字体源文件
     |└──── font.json // 转换后的字体文件
    ├── img // 素材图片
     |├──── xx.png
     |├──── xxx.jpg
     |└──── …
    ├── js // 自己编写的js文件
     |├──── composer_fn.js // 后期处理
     |├──── create_fn.js // 创建各种几何
     |├──── init_fn.js // 初始化项目
     |└──── util_fn.js // 工具函数
    ├── lib // 需要引入的js文件
     |├──── three.js
     |├──── OrbitControls.js
     |├──── RenderPass.js
     |└──── …
    ├── model // 建模工具导出的模型
     |├──── computer.gltf
     |└──── …
    └── index.html // 入口文件

    创建辉光效果

    有的时候我们希望某个几何体能更附加一层辉光特效,这样物体将更生动
    局部辉光

    效果合成器 EffectComposer

    要想实现辉光效果,就是实现后期处理效果,需要用到效果合成器 EffectComposer,所以我们新建一个 composer_fn.js 文件,专门用来写后期处理的函数,然后在 index.html 中引入该js文件

    // composer_fn.js
    function createComposer() {
      // 后期处理的通常步骤:
      //   1. 创建一个 EffectComposer,假设命名为composer
      //   2. 给composer加入(addPass)各种通道
      // 通常第一个加入的通道是RenderPass,后续可以看需求选择加入的通道类型和顺序,例如这里后续就用到了BloomPass
      const bloomComposer = new THREE.EffectComposer(renderer);
      const renderPass = new THREE.RenderPass(scene, camera);
      const bloomPass = createUnrealBloomPass(); // 我们封装好的 createUnrealBloomPass 函数,用来创建BloomPass(辉光效果)
      bloomComposer.addPass(renderPass);
      bloomComposer.addPass(bloomPass);
      return bloomComposer;
    }
    
    // UnrealBloomPass,辉光效果
    function createUnrealBloomPass() {
      const bloomPass = new THREE.UnrealBloomPass(
        new THREE.Vector2(window.innerWidth, window.innerHeight),
        1.5,
        0.4,
        0.85
      );
      const params = {
        exposure: 1,
        bloomThreshold: 0.2,
        bloomStrength: 0.5, // 辉光强度
        bloomRadius: 0,
      };
      bloomPass.threshold = params.bloomThreshold;
      bloomPass.strength = params.bloomStrength;
      bloomPass.radius = params.bloomRadius;
      return bloomPass;
    }
    

    除了在 index.html 引入该js文件,还需要引入效果合成器所需要的js文件(这些文件都能在ThreeJS的源码的example目录下找到),并且将render方法改用composer的render

    <!DOCTYPE html>
    <html>
      <head>...</head>
    
      <body>
        <script src="..."></script>
        
    	<!-- 后期处理,效果合成器所需的一些js文件 -->
    	<script src="lib/EffectComposer.js"></script>
    	<script src="lib/ShaderPass.js"></script>
    	<script src="lib/RenderPass.js"></script>
    	<script src="lib/CopyShader.js"></script>
    	<script src="lib/LuminosityHighPassShader.js"></script>
    	<script src="lib/UnrealBloomPass.js"></script>
    	
        <script>
          // ...
    	  const composer = createComposer();
    	  
    	  function animate() {
    	    // ...
    	    
    	    // renderer.render(scene, camera);
    	    composer.render(); // 将以前的render方法注释,换成composer的render
    	    
    	    requestAnimationFrame(animate);
    	  }		
    	  animate();
        </script>
      </body>
    </html>
    

    效果图:
    后期处理一

    部分辉光效果

    上面虽然实现了辉光效果,不过它将所有的物体,一切场景都添加了辉光,而我们的实际需求是只需要部分物体实现辉光

    部分辉光效果原理:

    1. 准备两个EffectComposer,一个 bloomComposer 用来产生辉光效果,另一个 finalComposer 用来正常渲染整个场景
    2. 将除辉光物体外的其他物体的材质转成黑色
    3. 在 bloomComposer 中利用 BloomPass 产生辉光,但这里需要设置 bloomComposer.renderToScreen = false; 表示不渲染到屏幕上
    4. 将转成黑色材质的物体还原成初始材质
    5. 用 finalComposer 开始渲染,其中 finalComposer 需要加入一个通道(addPass),该通道利用了 bloomComposer 的渲染结果

    Three中为所有的几何体分配 1个到 32 个图层,编号从 0 到 31,所有几何体默认存储在第 0 个图层上,为了更好的区分辉光物体和非辉光物体,我们需要利用 Layer 创建一个图层,把辉光物体额外添加在一个新的图层上

    // create_fn.js
    // 创建一个 Layer,用于区分辉光物体
    function createLayer(num) {
      const layer = new THREE.Layers();
      layer.set(num);
      return layer;
    }
    
    // 在 index.html 中使用
    const bloomLayer = createLayer(1); // 创建一个新的图层,编号为1
    
    // 然后往所有辉光物体中,添加一个新的图层,这里用我们之前写的机器为例
    // create_fn.js
    function createEarth(conf) {
      const geometry = new THREE.SphereBufferGeometry(5, 64, 64);
      const texture = new THREE.TextureLoader().load("./img/earth.png");
      const material = new THREE.MeshBasicMaterial({ map: texture });
      const mesh = new THREE.Mesh(geometry, material);
      initConfig(mesh, conf);
      mesh.layers.enable(1); // 与编号为1的图层建立关系,并切换到该图层。一定不能用 mesh.layers.set(1); 因为 set 会删除已有关系的图层,如果0图层没有了,那用 finalComposer 渲染的时候将渲染不了这个物体
      return mesh;
    }
    

    编写效果处理器代码

    // composer_fn.js
    function createComposer() {
      const renderPass = new THREE.RenderPass(scene, camera); // 两个composer都要用到这个renderPass,所以在前面公共部分声明
    
      // bloomComposer效果合成器 产生辉光,但是不渲染到屏幕上
      const bloomComposer = new THREE.EffectComposer(renderer);
      bloomComposer.renderToScreen = false; // 不渲染到屏幕上
      const bloomPass = createUnrealBloomPass();
      bloomComposer.addPass(renderPass);
      bloomComposer.addPass(bloomPass);
    
      // 最终真正渲染到屏幕上的效果合成器 finalComposer 
      const finalComposer = new THREE.EffectComposer(renderer);
      const shaderPass = createShaderPass(bloomComposer); // 创建自定义的着色器Pass,详细见下
      finalComposer.addPass(renderPass);
      finalComposer.addPass(shaderPass);
      return { bloomComposer, finalComposer };
    }
    
    // ShaderPass,着色器pass,自定义程度高,需要编写OpenGL代码
    // 传入bloomComposer
    function createShaderPass(bloomComposer) {
      // 着色器材质,自定义shader渲染的材质
      const shaderMaterial = new THREE.ShaderMaterial({
        uniforms: {
          baseTexture: { value: null },
          bloomTexture: { value: bloomComposer.renderTarget2.texture }, // 辉光贴图属性设置为传入的bloomComposer,这里就说明了为什么bloomComposer不要渲染到屏幕上
        },
        vertexShader: document.getElementById("vertexshader").textContent, // 顶点着色器
        fragmentShader: document.getElementById("fragmentshader").textContent, // 片元着色器
        defines: {},
      });
      const shaderPass = new THREE.ShaderPass(shaderMaterial, "baseTexture");
      shaderPass.needsSwap = true;
      return shaderPass;
    }
    

    在入口文件 index.html 中,运用效果处理器,实现部分辉光

    <!DOCTYPE html>
    <html>
      <head>...</head>
      <body>
      	<div>...</div>
      	
      	<!-- 着色器代码 -->
        <script type="x-shader/x-vertex" id="vertexshader">
          varying vec2 vUv;
          void main() {
          	vUv = uv;
          	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
          }
        </script>
        <script type="x-shader/x-fragment" id="fragmentshader">
          uniform sampler2D baseTexture;
          uniform sampler2D bloomTexture;
          varying vec2 vUv;
          vec4 getTexture( sampler2D texelToLinearTexture ) {
          	return mapTexelToLinear( texture2D( texelToLinearTexture , vUv ) );
          }
          void main() {
          	gl_FragColor = ( getTexture( baseTexture ) + vec4( 1.0 ) * getTexture( bloomTexture ) );
          }
        </script>
        
        <script src="..."></script>
        <script>
          // ...
          const bloomLayer = createLayer(1); // 创建一个新的图层,编号为1
          const materials = {};
          const darkMaterial = new THREE.MeshBasicMaterial({ color: "black" }); // 提前创建好黑色普通材质,供后面使用
    	  const { bloomComposer, finalComposer } = createComposer(); // 创建效果处理器
    
          function animate(time) {
            // ...
    
            // 实现局部辉光
            // 1. 利用 darkenNonBloomed 函数将除辉光物体外的其他物体的材质转成黑色
            scene.traverse(darkenNonBloomed);
            // 2. 用 bloomComposer 产生辉光
            bloomComposer.render();
            // 3. 将转成黑色材质的物体还原成初始材质
            scene.traverse(restoreMaterial);
            // 4. 用 finalComposer 作最后渲染
            finalComposer.render();
    
            requestAnimationFrame(animate);
          }
          // 将场景中除了辉光物体外的物体材质转成黑色
          function darkenNonBloomed(obj) {
            // layer的test方法是判断参数中的图层和自己的图层是否是同一个图层
            // 如果obj是几何体,且不在bloomLayer图层,说明不是辉光物体
            if ((obj.isMesh || obj.isSprite) && bloomLayer.test(obj.layers) === false) {
              // 如果是精灵几何体,需要转成黑色的精灵材质,做特殊处理
              if (obj.isSprite) {
                materimals[obj.uuid] = obj.material; // 在materimals变量中保存原先的材质信息
                obj.material = new THREE.SpriteMaterial({
                  color: "#000",
                });
              // 其他几何体可以转成普通的黑色材质
              } else {
                materials[obj.uuid] = obj.material; // 在materimals变量中保存原先的材质信息
                obj.material = darkMaterial;
              }
            }
          }
          // 将场景中材质转成黑色的物体还原
          function restoreMaterial(obj) {
            if (materials[obj.uuid]) {
              obj.material = materials[obj.uuid]; // 还原材质
              delete materials[obj.uuid]; // 内存中删除
            }
          }
    	  animate();
        </script>
      </body>
    </html>
    

    效果图:
    部分辉光

    加入抗锯齿

    终于我们实现了部分辉光,不过细心的我们突然发现,加入BloomPass之后,物体的锯齿严重,即使我们在render中设置了antialias抗锯齿属性依然如此,所以这里我引入了另一个后期处理的通道FxaaPass

    // composer_fn.js
    function createComposer() {
      const renderPass = new THREE.RenderPass(scene, camera); 
    
      const bloomComposer = new THREE.EffectComposer(renderer);
      bloomComposer.renderToScreen = false;
      const bloomPass = createUnrealBloomPass();
      bloomComposer.addPass(renderPass);
      bloomComposer.addPass(bloomPass);
    
      const finalComposer = new THREE.EffectComposer(renderer);
      const shaderPass = createShaderPass(bloomComposer);
      const FxaaPass = createFxaaPass(); // 我封装的创建 FxaaPass 的函数,详细见下
      finalComposer.addPass(renderPass);
      finalComposer.addPass(shaderPass);
      finalComposer.addPass(FxaaPass);
      
      return { bloomComposer, finalComposer };
    }
    
    // 抗锯齿,fxaa、smaa、ssaa都可以抗锯齿,抗锯齿效果依次减弱
    function createFxaaPass() {
      let FxaaPass = new THREE.ShaderPass(THREE.FXAAShader);
      const pixelRatio = renderer.getPixelRatio();
      FxaaPass.material.uniforms["resolution"].value.x =
        1 / (window.innerWidth * pixelRatio);
      FxaaPass.material.uniforms["resolution"].value.y =
        1 / (window.innerHeight * pixelRatio);
      FxaaPass.renderToScreen = true;
      return FxaaPass;
    }
    

    在入口文件 index.html 引入新的依赖文件

    <!-- index.html -->
    <script src="lib/FXAAShader.js"></script>
    

    加了抗锯齿之后,整个画面平滑了很多
    抗锯齿

    高级效果组合器 MaskPass

    终于又解决了抗锯齿问题,总算可以歇口气了……等等,为什么精灵文字突然就变糊了,看来这样还不够,在我多番研究下,最终我想到了用 [高级效果组合器 MaskPass] 解决这个问题

    MaskPass 是什么呢?简单来说,就是可以在一个 EffectComposer 中进行Mask分组,每一组Mask使用不同的通道(也就是每一组 addPass 的内容不一样),并且每一组Mask可以渲染在不同的Scene场景

    实现原理:

    1. 将没有糊掉的部分设为第一组Mask,采用原先的通道:加辉光、抗锯齿
    2. 将糊掉的部分设为第二组Mask,采用新的通道:不做任何处理,直接渲染。这组中有精灵文字和辉光(辉光不需要加辉光和抗锯齿,相反如果放在上一组容易产生色差等意料外的bug,所以也放在不做任何处理这一组里)

    下面是具体实现过程:
    3. 创建好两个Scene,然后将不做任何处理、直接渲染的第二组Mask中的几何体,从Group分组中摘出来(之前每个Group分组都有一个精灵文字,辉光也放在第二组Group中),并加入到 normalScene 中,其余的保留在 scene 里

    // init_fn.js
    function initThree(selector) {
      const scene = new THREE.Scene();
      const normalScene = new THREE.Scene(); // 创建两个scene
      const camera = ...;
      const renderer = ...;
      renderer.autoClear = false; // 这里注意!!需要手动清除,要使用高级效果组合器MaskPass,autoClear 必须设置为false
      document.querySelector(selector).appendChild(renderer.domElement);
      return { scene, normalScene, camera, renderer };
    }
    

    入口文件 index.html

    <!DOCTYPE html>
    <html>
      <head>...</head>
    
      <body>
        <script src="..."></script>
        <script>
    	  const { scene, normalScene, camera, renderer } = initThree("#canvas-frame");
    	  // ...
    	  let group1, group1Animate;
    	  {
    	    // ...
    	  }
    	  
    	  let group2, group2Animate;
    	  {
    	    // ...
    	  }
    
          // normalScene场景的内容
          let normalSceneAnimate;
          {
            const { sprite: spriteText1 } = await createSpriteText("#label1", {
              position: { x: -65, y: 23 },
            }); // 摘出来的原先 Group1 中的精灵文字
            const { sprite: spriteText2 } = await createSpriteText("#label2", {
              position: { x: 36, y: 23 },
            }); // 摘出来的原先Group2 中的精灵文字
            const beam = createLightBeam(100, 56, 2, "red", {
              scale: { z: 10 },
              rotation: { x: Math.PI / 2 },
              position: { x: -13, y: 3.9, z: -28 },
            }); // 摘出来的原先 Group2 中的光圈效果
           normalScene.add(spriteText1);
           normalScene.add(spriteText2);
           normalScene.add(beam); // 全部加入到 normalScene 中
           
           let direction = true;
           normalSceneAnimate = function () {
             if (direction) {
                beam.material[1].opacity -= 0.01;
                if (beam.material[1].opacity <= 0.5) {
                  direction = false;
                }
             } else {
                beam.material[1].opacity += 0.01;
                if (beam.material[1].opacity >= 1) {
                  direction = true;
                }
              }
            };
          }
    	  function animate() {
    	    group1Animate();
            group2Animate();
            normalSceneAnimate();
             
            stats.update();
    	    
    	    // 渲染过程依然不变,变得只是 EffectComposer 的渲染分组 Mask
    	    scene.traverse(darkenNonBloomed);
            bloomComposer.render();
            scene.traverse(restoreMaterial);
            finalComposer.render();
    	    composer.render();
    	    
    	    requestAnimationFrame(animate);
    	  }		
    	  animate();
        </script>
      </body>
    </html>
    
    1. 修改 EffectComposer 的逻辑,利用 MaskPass 分组渲染
    // composer_fn.js
    function createComposer() {
      const renderPass = new THREE.RenderPass(scene, camera); // 第一个分组的RenderPass
      const renderNormalPass = new THREE.RenderPass(normalScene, camera); // 第二个分组的RenderPass
    
      // 产生辉光,但是不渲染到屏幕上
      const bloomComposer = new THREE.EffectComposer(renderer);
      bloomComposer.renderToScreen = false;
      const bloomPass = createUnrealBloomPass();
      bloomComposer.addPass(renderPass);
      bloomComposer.addPass(bloomPass);
    
      // 利用 MaskPass 最终渲染到屏幕上
      const finalComposer = new THREE.EffectComposer(renderer);
      finalComposer.renderTarget1.stencilBuffer = true;
      finalComposer.renderTarget2.stencilBuffer = true; // 两个都设置为true,这一步不能省
      renderPass.clear = false;
      renderNormalPass.clear = false; // 这两句非常重要,RenderPass默认为false,如果这里是false,那么renderNormalPass 会清除掉上一个 RenderPass —— renderPass 的颜色
      finalComposer.addPass(renderPass);
      finalComposer.addPass(renderNormalPass);
      
      const clearMaskPass = new THREE.ClearMaskPass();
      // 第一组开始渲染
      const maskPass1 = new THREE.MaskPass(scene, camera);
      const shaderPass = createShaderPass(bloomComposer);
      const FxaaPass = createFxaaPass();
      finalComposer.addPass(maskPass1); // 添加第一组的maskPass
      finalComposer.addPass(shaderPass);
      finalComposer.addPass(FxaaPass);
      finalComposer.addPass(clearMaskPass); // 清除第一组的maskPass
    
      // 第二组开始渲染
      const maskPass2 = new THREE.MaskPass(normalScene, camera);
      finalComposer.addPass(maskPass2); // 添加第二组的maskPass
      finalComposer.addPass(clearMaskPass); // 添加第二组的maskPass
    
      const effectCopy = new THREE.ShaderPass(THREE.CopyShader);
      finalComposer.addPass(effectCopy); // 最后需要CopyShader,因为设置了手动清除
      return { bloomComposer, finalComposer };
    }
    

    最终效果:既有部分辉光效果,又有抗锯齿效果,还不会让部分物体变糊
    最终效果
    需要特别注意的几点:
    5. renderer 的 autoClear 设为 false
    6. EffectComposer 的 renderTarget2.stencilBuffer 设为true
    7. RenderPass 的 clear 设为 false
    8. 因为设置了手动Clear,所以最后需要 addPass 一个 CopyShader

    另一种简单的辉光实现

    这种方法是利用 ShaderMaterial 写一个发光材质
    在这里插入图片描述
    着色器核心代码:

     void main() 
        {
          float a = pow( bias + scale * abs(dot(vNormal, vPositionNormal)), power );
          gl_FragColor = vec4( glowColor, a );
        }
    

    完整代码:

    <script id="vertexShader1" type="x-shader/x-vertex">
      varying vec3 vNormal;
      varying vec3 vPositionNormal;
      void main() 
      {
        vNormal = normalize( normalMatrix * normal ); // 转换到视图空间
        vPositionNormal = normalize(( modelViewMatrix * vec4(position, 1.0) ).xyz);
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    </script>
    <!-- fragment shader a.k.a. pixel shader -->
    <script id="fragmentShader1" type="x-shader/x-vertex">
      uniform vec3 glowColor;
      uniform float b;
      uniform float p;
      uniform float s;
      varying vec3 vNormal;
      varying vec3 vPositionNormal;
      void main() 
      {
        float a = pow( b + s * abs(dot(vNormal, vPositionNormal)), p );
        gl_FragColor = vec4( glowColor, a );
      }
    </script>
    
    const customMaterial = new ShaderMaterial({
      uniforms: {
        "s": { type: "f", value: -1.0 },
        "b": { type: "f", value: 1.0 },
        "p": { type: "f", value: 2.0 },
        glowColor: { type: "c", value: new Color(0x00ffff) }
      },
      vertexShader: document.getElementById('vertexShader1').textContent,
      fragmentShader: document.getElementById('fragmentShader1').textContent,
      side: FrontSide,
      blending: AdditiveBlending,
      transparent: true
    })
    

    参考文章:https://zhuanlan.zhihu.com/p/38548428

    展开全文
  • ThreeJS —— 机房Demo(三)目录结构创建文字创建3D文字创建精灵文字Sprite改进版精灵文字创建光圈效果 上一节我们重构了我们的代码,形成了两个区域,绘制出了一个大致的场景,这一节我们将在此基础上再添加一些...

    上一节我们重构了我们的代码,形成了两个区域,绘制出了一个大致的场景,这一节我们将在此基础上再添加一些实用的场景

    目录结构

    ├── font // 字体文件
     |├──── font.ttf // 字体源文件
     |└──── font.json // 转换后的字体文件
    ├── img // 素材图片
     |├──── xx.png
     |├──── xxx.jpg
     |└──── …
    ├── js // 自己编写的js文件
     |├──── composer_fn.js // 后期处理
     |├──── create_fn.js // 创建各种几何
     |├──── init_fn.js // 初始化项目
     |└──── util_fn.js // 工具函数
    ├── lib // 需要引入的js文件
     |├──── three.js
     |├──── OrbitControls.js
     |├──── RenderPass.js
     |└──── …
    ├── model // 建模工具导出的模型
     |├──── computer.gltf
     |└──── …
    └── index.html // 入口文件

    创建文字

    一个场景中一定少不了文字说明,用来描述该区域的特点、情况

    创建3D文字

    // create_fn.js
    function createText(text, color, conf) {
      // 为了解决回调地狱,同样引入Promise
      return new Promise((res) => {
        new THREE.FontLoader().load("../font/simhei.json", function (font) {
          const geometry = new THREE.TextBufferGeometry(text, {
            font,
            size: 3,
            height: 1,
            curveSegments: 64,
          });
          geometry.center(); // 将文字居中
          const material = new THREE.MeshBasicMaterial({
            color,
          });
          const mesh = new THREE.Mesh(geometry, material);
          initConfig(mesh, conf);
          res(mesh);
        });
      });
    }
    
    // index.html
    // 引用
    const text = await createText("移动网络接入区", "rgb(216, 120, 133)", {
      position: { x: 39, y: -3, z: 22 },
    });
    

    效果图:
    3D文字

    创建精灵文字Sprite

    有的时候我们需要创建一个始终面向我们的一个平面,这个时候就要用的精灵几何体Sprite,精灵是一个总是面朝着摄像机的平面

    效果图:
    精灵文字
    要创建一个Sprite几何体,必须传入SpriteMaMterial精灵材质,而该材质支持贴图,所以我们通常用图片或画布贴图实现Sprite,不过为了更自由的配置Sprite的内容(用图片太不方便,不同的Sprite还需要制作不同的图片),我们这里采用画布贴图CanvasTexture,并用到了一个DOM转canvas的插件html2canvas

    // create_fn.js
    // 创建永远朝向自己这一面的文字
    async function createSpriteText(selcetor, conf) {
      const elem = document.querySelector(selcetor); // selector是传入的选择器
      const canvas = await html2canvas(elem, {
        // 加入x、y配置,防止画布偏移,不加这两个配置,画布有可能偏移,产生空白区域
        x: elem.offsetLeft, 
        y: elem.offsetTop,
      });
      const texture = new THREE.CanvasTexture(canvas); 
      texture.magFilter = THREE.NearestFilter; // 提高清晰度,不加这两句画布会变模糊
      texture.minFilter = THREE.NearestFilter;
      const spriteMaterial = new THREE.SpriteMaMterial({
        map: texture,
        opacity: 0.8,
      }); // 创建精灵材质,map属性设置贴图,为了更高的可配置度,我们选择用canvas贴图
      const sprite = new THREE.Sprite(spriteMaterial); // 要创建精灵几何体必须要用精灵材质
      initConfig(sprite, conf);
      return sprite;
    }
    

    在入口文件index.html中使用createSpriteText

    <!DOCTYPE html>
    <html>
      <head>
      	<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
        <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous"
        />
        <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
        <script
          src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
          integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
          crossorigin="anonymous"
        ></script>
        <style type="text/css">
          /* ... */
          .panel {
            border: 0;
            width: 270px;
            text-indent: 20px;
            font-family: "tencent";
          }
        </style>
      </head>
    
      <body>
        <div id="canvas-frame"></div>
    
        <!-- 精灵文字模板,这里利用bootstrap快捷实现一个模板 -->
        <div class="panel" id="label">
          <div class="panel-heading" style="background-color: rgba(161, 89, 41, 0.8); color: white;">专线网络接入区</div>
          <div class="panel-body" style="background-color: rgba(72, 58, 46, 0.8); color: white;">
            <p>区域机器总数:100</p>
            <p>高风险漏洞机器总数:10</p>
            <p>高风险漏洞机器占比:10%</p>
          </div>
        </div>
    
    	<!-- 引入的一些JS -->
        <script src="lib/three.js"></script>
        <script src="..."></script>
        
        <script>
          const { scene, camera, renderer } = initThree(
            "#canvas-frame"
          );
          // ...
          
          // 新添加的代码
          const sprite = await createSpriteText("#label", {
            position: { x: -65, y: 23 },
            scale: { x: 25, y: 15 },
          });
          group1.add(sprite);
    
    	  // 最后要把精灵文字的模板从body元素中移除
    	  document.body.removeChild(document.querySelector("#label"));
        </script>
      </body>
    </html>
    

    这样我们就实现了精灵文字,不过如果我们想实现带弧角的矩形怎么做呢?

    相信很多人想,在dom元素中加一个border-radius不就行了,不过这是dom元素的border,然而sprite默认的geometry属性是一个正常的四四方方的矩形,这样会在背景留白,如图:
    改进精灵文字

    改进版精灵文字

    这里就需要我们在创建好Sprite后,手动去修改Sprite下的geometry属性,用新的geometry去替换旧的

    // create_fn.js
    async function createSpriteText(selcetor, conf) {
      const elem = document.querySelector(selcetor);
      const canvas = await html2canvas(elem, {
        x: elem.offsetLeft,
        y: elem.offsetTop,
      });
      const texture = new THREE.CanvasTexture(canvas);
      texture.magFilter = THREE.NearestFilter; // 提高清晰度
      texture.minFilter = THREE.NearestFilter;
      const spriteMaterial = new THREE.SpriteMaterial({
        map: texture,
        opacity: 0.8,
      });
      const sprite = new THREE.Sprite(spriteMaterial);
      const canvasW = canvas.width;
      const canvasH = canvas.height;
      const shape = createArcRect((15 * canvasW) / canvasH, 15, 2.5); // createArcRect是我们上一节封装的函数,用来创建一个弧角矩形的形状,三个参数分别代表长、宽、弧度,这里长宽按canvas比例缩小,不能用原有的长宽,否则过大
      const geometry = new THREE.ShapeBufferGeometry(shape, 64); // 创建一个自定义形状的平面
      sprite.geometry = geometry; // 用我们创建好的弧角矩形平面代替Sprite默认的geometry
      initConfig(sprite, conf);
      return sprite;
    }
    

    不过这里虽然是变成了弧角矩形,但并没有像我们想的那样,是为什么呢?
    改进精灵文字
    这里就涉及到了图形学中非常重要的一个概念——UV坐标,在利用ShapeGeometry自定义形状贴图时,如果使用纯色贴图到不会产生预料之外的偏差,不过如果用纹理贴图就会产生一个问题:因为我们的模型是根据一个shape生成的ShapeGeometry,所以贴图会采用UV坐标进行贴图,UV坐标是一组在(0,1)范围内的坐标,更多有关UV坐标的解释可以参考这篇文章,所以这里我们需要计算并更新uv坐标。

    // create_fn.js
    async function createSpriteText(selcetor, conf) {
      // ...
      const shape = createArcRect((15 * canvasW) / canvasH, 15, 2.5); 
      const geometry = new THREE.ShapeBufferGeometry(shape, 64); 
      computeUV(geometry); // 计算并更新该几何体的UV
      sprite.geometry = geometry; 
      initConfig(sprite, conf);
      return sprite;
    }
    
    // util_fn.js
    // 计算对应UV坐标
    function computeUV(geometry) {
      geometry.computeBoundingBox(); // 计算外边界矩形,这样才能得到geometry的boundingBox属性值
      const max = geometry.boundingBox.max,
        min = geometry.boundingBox.min; // 获取最大、最小值
      const offset = new THREE.Vector2(0 - min.x, 0 - min.y); // 计算偏移量
      const range = new THREE.Vector2(max.x - min.x, max.y - min.y); // 计算范围
      const uvArr = geometry.getAttribute("uv");
      uvArr.array = uvArr.array.map((item, index) =>
        index % 2 ? item / range.y + offset.y : item / range.x + offset.x
      );
      geometry.setAttribute("uv", uvArr); // 将geometry的uv属性设置成我们刚刚计算出来的新uv值
      geometry.uvsNeedUpdate = true; // needUpdate必须为true才会更新
    }
    

    这样终于就满足了我们的需求
    精灵文字最终版

    创建光圈效果

    在机房场景中,我们有的时候需要一圈光来表示这片区域的情况,例如绿色表示正常,红色表示告警
    在这里插入图片描述
    原理:利用 ExtrudeGeometry(ExtrudeGeometry是将一个平面延伸后得到的一个几何体),并用一张渐变色的图片贴图,即可得到光圈效果

    渐变素材图片:
    渐变色

    // create_fn.js
    // 创建围绕物体的辉光效果
    function createLightBeam(width, height, arc, color, conf) {
      const shape = createArcRect(width, height, arc); // createArcRect是我们上一节封装的函数,用来创建一个弧角矩形的形状
      const extrudeSettings = {
        steps: 64,
        depth: 1, // step设置为1,保证侧面只有一个平面,如果设置大于1,则延伸出去的侧面不止一个平面,导致贴图时会产生bug,如果想延伸的更深,可以通过scale放大
        bevelEnabled: false,
      };
      const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
      const bottomMaterial = new THREE.MeshBasicMaterial({
        visible: false,
      }); // 设置上下底面的材质不可见
      const texture = createTexture("img/gradient.png");
      const sideMaterial = new THREE.MeshBasicMaterial({
        map: texture,
        side: THREE.DoubleSide,
        transparent: true,
        opacity: 1,
        depthWrite: true,
        color,
      }); // 给侧面进行贴图,贴图的图片为一张渐变色的图片
      const mesh = new THREE.Mesh(geometry, [bottomMaterial, sideMaterial]);
      initConfig(mesh, conf);
      return mesh;
    }
    

    为光圈添上动画

    到这里,光圈效果就产生了,我们还可以给其加上动画,让其渐隐渐显

    <!DOCTYPE html>
    <html>
      <head>...</head>
    
      <body>
    	...
        <script>
          // ...
          
          // 新添加的代码
       	  const beam = createLightBeam(100, 56, 2, "red", {
    	    scale: { z: 10 },
    	    rotation: { x: Math.PI / 2 },
    	    position: { x: -13, y: 3.9, z: -28 },
    	  });
    	  scene.add(beam); // 这里是放在scene中,实际我放在了group2分组下
    	
    	  // 控制动画
    	  let direction = true;
    	  function animate() {
    	    // ...
    	
    	    if (direction) {
    	      beam.material[1].opacity -= 0.01;
    	      if (beam.material[1].opacity <= 0.5) {
    	        direction = false;
    	      }
    	    } else {
    	      beam.material[1].opacity += 0.01;
    	      if (beam.material[1].opacity >= 1) {
    	        direction = true;
    	      }
    	    }
    	  
    	    renderer.render(scene, camera);
    	    requestAnimationFrame(animate);
    	  }		
    	  animate();
        </script>
      </body>
    </html>
    

    利用Tween.js实现动画

    手动实现动画始终是比较麻烦的,我们可以用 tween.js 这个补间动画库来快捷实现

    <!DOCTYPE html>
    <html>
      <head>...</head>
    
      <body>
    	...
        <script>
          // ...
       	  const beam = ...;
    	
    	  // tween实现动画
    	  const tween1 = new TWEEN.Tween(beam.material[1])
            .to({ opacity: 0 }, 1000)
            .onComplete(() => {
              tween2.start(); // 结束后调用tween2,开始显示
            }); // 渐隐动画
          const tween2 = new TWEEN.Tween(beam.material[1])
            .to({ opacity: 1 }, 1000)
            .onComplete(() => {
              tween1.start(); // 结束后调用tween1,开始隐藏
            }); // 渐显动画
          tween1.start();
          
    	  function animate() {
    	    // ...
    	
    	    TWEEN.update(); // 必须加上这一句
    	  
    	    renderer.render(scene, camera);
    	    requestAnimationFrame(animate);
    	  }		
    	  animate();
        </script>
      </body>
    </html>
    
    展开全文
  • ThreeJS —— 机房Demo(五)目录结构性能优化提取公共部分利用 clone 方法复用减少 animate 内容引入模型优化 上一节我们基本完成了我们Demo所需要的所有功能,离成功只差最后一步 —— 性能优化! 目录结构 ├──...

    上一节我们基本完成了我们Demo所需要的所有功能,离成功只差最后一步 —— 性能优化

    目录结构

    ├── font // 字体文件
     |├──── font.ttf // 字体源文件
     |└──── font.json // 转换后的字体文件
    ├── img // 素材图片
     |├──── xx.png
     |├──── xxx.jpg
     |└──── …
    ├── js // 自己编写的js文件
     |├──── common_fn.js // 公共部分
     |├──── composer_fn.js // 后期处理
     |├──── create_fn.js // 创建各种几何
     |├──── init_fn.js // 初始化项目
     |└──── util_fn.js // 工具函数
    ├── lib // 需要引入的js文件
     |├──── three.js
     |├──── OrbitControls.js
     |├──── RenderPass.js
     |└──── …
    ├── model // 建模工具导出的模型
     |├──── computer.gltf
     |└──── …
    └── index.html // 入口文件

    性能优化

    ThreeJS常用性能优化方法

    提取公共部分

    在构建Demo的过程,我们需要创建很多几何体 Geometry 和材质 Material,以及一些 Loader 等等,这些资源都是可以单独提取成公共部分的

    // 创建一个common_fn.js,放在js目录下
    // common_fn.js
    
    // MeshBasicMaterial 这个材质经常使用,并且该黑色材质被多次使用
    const blackBasicMaterial = window.blackBasicMaterial = new THREE.MeshBasicMaterial({ color: "black" });
    
    // TextureLoader 也是一个常用的 Loader,先创建一个实例出来供后面共用(这里也是参考了享元模式)
    const textureLoader = window.textureLoader = new THREE.TextureLoader();
    
    // 后续代码中如要使用,直接用变量名即可,因为放入了全局变量
    

    利用 clone 方法复用

    Demo中有很多几何体都是同一种类型,只是他们的位置不一样,所以我们可以通过 Mesh 的 clone 方法实现复用的目的,复用后修改位置即可

    // 在 create_fn.js 中构建一个 createClone 方法
    // create_fn.js
    function createClone(mesh, conf) {
      const newMesh = mesh.clone();
      initConfig(newMesh, conf);
      return newMesh;
    }
    

    在入口文件 index.html 中使用

    <!DOCTYPE html>
    <html>
      <head>...</head>
      <body>
        <div id="canvas-frame"></div>
        
        <script>...</script>
        <script>
          // ...
          (async function () {
            // scene场景的公共内容,用于后面 createClone 公用
            const earth = createEarth({ position: { x: -15, y: -1 } });
            const machine = createMachine("./img/move.png", {
              position: { x: 15, z: -20, y: -5 },
            });
    
            // scene场景的第一组内容
            let group1, group1Animate;
            {
              const earth2 = createClone(earth, { position: { x: 15, y: -1 } });
              const machine2 = createClone(machine, {
                position: { x: -15 },
              });
              // ...
            }
    
            // scene场景的第二组内容
            {
              const earth1 = createClone(earth, {
                position: { x: 0, z: -10, y: -1.1 },
              });
              const machine1 = createClone(machine, {
                position: { x: 0, y: -5, z: 10 },
              });
              // ...
            }
    
            // ...
          })();
        </script>
      </body>
    </html>
    

    减少 animate 内容

    之前我们的 animate 方法中有如下内容:

    // index.html
    function animate() {
      // 管道运动,路线循环流动效果
      group1Animate();
      group2Animate();
      commonAnimate();
      normalSceneAnimate();
    
      // fps监控
      stats.update();
    
      // 实现局部辉光
      scene.traverse(darkenNonBloomed);
      bloomComposer.render();
      scene.traverse(restoreMaterial);
      finalComposer.render();
    
      requestAnimationFrame(animate);
    }
    

    这里我们发现,实现局部辉光的过程,如果我们不转动控制器,就不需要每次 requestAnimationFrame 的时候都调用 bloomComposer.render(),所以对入口文件 index.html 优化如下

    <!DOCTYPE html>
    <html>
      <head>...</head>
      <body>
        <div id="canvas-frame"></div>
        
        <script>...</script>
        <script>
          // ...
    
          // 产生局部辉光的前三步,初始状态必须先调用 bloomComposer.render()
          scene.traverse(darkenNonBloomed);
          bloomComposer.render();
          scene.traverse(restoreMaterial);
    
          controls.addEventListener("change", function () {
            // 产生局部辉光的前三步,操作控制器的时候重新调用 bloomComposer.render() 更新辉光
            scene.traverse(darkenNonBloomed);
            bloomComposer.render();
            scene.traverse(restoreMaterial);
          });
    
          function animate() {
              // 管道运动,路线循环流动效果
              group1Animate();
              group2Animate();
              commonAnimate();
              normalSceneAnimate();
    
              // fps监控
              stats.update();
    
              // 实现局部辉光
              finalComposer.render(); // 因为有动画的存在,每帧依然要执行渲染函数 render,这里不能省
    
              requestAnimationFrame(animate);
            }
        </script>
      </body>
    </html>
    

    除此之外,在每帧动画中,我们也应该减少更新,这里的更新指的是当前的几何体、材质、纹理等发生了修改,需要Three.js重新更新显存的数据,具体包括:

    • 几何体:
    geometry.verticesNeedUpdate = true; //顶点发生了修改
    geometry.elementsNeedUpdate = true; //面发生了修改
    geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
    geometry.uvsNeedUpdate = true; //uv映射发生了修改
    geometry.normalsNeedUpdate = true; //法向发生了修改
    geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
    
    • 材质
    material.needsUpdate = true
    
    • 纹理
    texture.needsUpdate = true;
    

    如果它们发生更新,则将其设置为true,Three.js会通过判断,将数据重新传输到显存当中,并将配置项重新修改为false。此外,我们可以利用函数的节流防抖,对 animate 函数进行优化

    引入模型优化

    通常项目中,需要引入第三方软件建模后的模型,例如这个Demo中也引入了电脑的模型,模型文件通常很大,如何压缩模型文件,加快引入时间也是优化的重要一环

    1. 利用 obj2gltf 插件把 obj 格式的模型转成 gltf 格式,用法如下:
      obj2gltf  -i ./xxx.obj -o ./xxx.gltf --unlit --separate
      
      --unlit 表示保留环境贴图的效果
      --separate 表示将贴图文件提取出来,浏览器可以缓存,如果你需要继续压缩gltf文件,这里可以不加,因为后续压缩的时候也能提出来
      
    2. 利用 gltf-pipeline 插件把 gltf 格式的模型进行压缩,用法如下:
      gltf-pipeline -i  ./xxx.gltf  -o  ./xxx.gltf -d --separate
      
      -d是--draco.compressMeshes的缩写,使用draco算法压缩模型
      --separate就是将贴图文件提取出来,不提可以不加
      

    引入字体优化

    之前我们创建3D文字的时候,是利用官方推荐的 facetype.js 将 ttf 格式的字体转成 json 格式后引入的,不过实际运用时发现,转成 json文件后,文件大了很多倍,导致加载时间非常长
    json格式
    在尝试了压缩 json 文件,修改转换配置等方法无果后,我在 example 中看到了一种新的引入字体的方式,即利用 TTFLoader 直接加载 ttf 文件

    首先在 index.html 引入所需文件

    <script src="lib/opentype.min.js"></script>
    <script src="lib/TTFLoader.js"></script>
    

    加载字体

    new THREE.TTFLoader().load("../font/simhei.ttf", function (data) {
      const font = new THREE.Font(data);
      const geometry = new THREE.TextBufferGeometry(text, {
        font,
        size: 3,
        height: 1,
        curveSegments: 64,
      });
      // ...
    });
    

    改用 TTFLoader 后,加载速度有了明显的提升
    ttf格式

    减少请求次数

    由于需要引入额外的很多 js 文件,导致请求次数特别多,因此我们需要打包工具 webpack 帮助我们打包压缩(为了利用 webpack 打包,我们需要将原项目重构成 CommonJS 规范,详情见下文)

    webpack 配置

    // webpack.config.js
    const path = require("path");
    const webpack = require("webpack");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const { CleanWebpackPlugin } = require("clean-webpack-plugin");
    const CopyWebpackPlugin = require('copy-webpack-plugin');
    
    module.exports = {
      entry: "./main.js",
      output: {
        path: path.resolve(__dirname, "./dist")
      }
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          template: "./public/index.html",
          favicon: path.resolve("./public/favicon.ico"),
        }),
        new webpack.HotModuleReplacementPlugin(),
        new CopyWebpackPlugin({
          patterns: [
            {
              from: 'src/assets/',
              to: 'assets/'
            },
          ],
        })
      ],
      devServer: {
        open: true,
        port: "8080",
        hot: true,
        hotOnly: true,
      }
    };
    

    使用BufferGeometry

    重构为 CommonJS 规范

    目录结构:

    ├─ node_modules
    ├─ public // 类似 vue 的 public 目录
     |   ├─ favicon.ico // 网站图标
     |   └─ index.html // 模板html
    ├─ src // 源文件
     |   ├─ assets // 静态资源
     |    |   ├─ font // 字体文件夹
     |    |    |   └─ …
     |    |   ├─ img // 图片文件夹
     |    |    |   └─ …
     |    |   ├─ model // 模型文件夹
     |    |    |   └─ …
     |   ├─ composer
     |    |   └─ composer.js // 后期处理 EffectComposer
     |   ├─ group
     |    |   ├─ groupCommon.js // 分组间公用的内容
     |    |   ├─ normalSceneGroup.js // 分组一
     |    |   ├─ sceneGroup1.js // 分组二
     |    |   └─ sceneGroup2.js // 分组三
     |   ├─ js // 存放自己编写的 js 文件
     |    |   ├─ common.js // 全局变量,注入在 Gvo 类的属性中
     |    |   ├─ create.js // 创建各种几何体物体
     |    |   └─ util.js // 工具函数
     |   ├─ lib // 存放各种需要的引入的文件
     |    |   ├─ RenderPass.js
     |    |   ├─ three.module.js
     |    |   ├─ tween.cjs.js
     |    |   └─ …
     |   └─ gvo.js // Gvo 类
    ├─ main.js // 入口文件
    ├─ package.json
    └─ webpack.config.js // webpack配置

    首先我们在 gvo.js 创建一个类 Gvo,将之前的 init_fn 中的方法写在 Gvo 中,并在 constructor 构造方法里执行初始化

    // gvo.js
    class Gvo {
      constructor(selector, ThreeOption, lightOption) {
        this.initThree(selector, ThreeOption); // 默认自动初始化
        this.initLight(lightOption); // 默认自动初始化
        this.customInit(); // 用户自定义初始化
        this.initControls(); // 默认自动初始化
        this.initStats(); // 默认自动初始化
      }
      // 用户自定义初始化
      customInit() { ... }
      // 初始化三大件:场景、相机、渲染器
      initThree(selector, { cameraOption, rendererOption } = {}) { ... }
      // 设置灯光
      initLight(lightOption = 0xffffff) { ... }
      // 添加控制器
      initControls() { ... }
      // 添加fps
      initStats() { ... }
    }
    

    再将之前的 create_fn 中的一系列create方法绑定在 Gvo 类的方法上

    // create.js
    function createClone() { ... }
    function createImportModel() { ... }
    function createEarth() { ... }
    function createMachine() { ... }
    ...
    
    Gvo.prototype.createClone = createClone;
    Gvo.prototype.createImportModel = createImportModel;
    Gvo.prototype.createEarth = createEarth;
    Gvo.prototype.createMachine = createMachine;
    ...
    

    然后将之前的 util_fn 中的方法 export 导出,在 composer_fn 中加入 darkenNonBloomed 和 restoreMaterial 方法(这两个方法是直接写在入口文件 index.html 中的),连同 createComposer 方法一并导出

    // composer.js
    function createComposer() { ... }
    ...
    function darkenNonBloomed() { ... }
    function restoreMaterial() { ... }
    module.exports = { createComposer, darkenNonBloomed, restoreMaterial };
    
    // util.js
    function mergeImage() { ... }
    function computeUV() { ... }
    module.exports = { mergeImage, computeUV }
    

    接着新建一个group文件夹,在里面写上所有分组的逻辑,这里举例 groupCommon.js 和 sceneGroup1.js

    // groupCommon.js
    // scene场景的公共内容
    module.exports = (gvonte) => {
        const earth = gvonte.createEarth({ position: { x: -15, y: -1 } });
        const machine = gvonte.createMachine("./assets/img/move.png", { position: { x: 15, z: -20, y: -5 } });
        gvonte.groupCommon = { earth, machine };
    };
    
    // sceneGroup1.js
    // scene场景的第一组内容
    module.exports = async (gvonte) => {
        const { earth, machine } = gvonte.groupCommon;
        const earth1 = gvonte.createClone(earth, { position: { x: 15, y: -1 } });
        const machine1 = gvonte.createClone(machine, {
            position: { x: -15 },
        });
        const machine2 = gvonte.createClone(machine, {
            rotation: { x: Math.PI / 2 },
            position: { x: -15, y: -1, z: 15 },
        });
        // ...
        const group1 = gvonte.createGroup(
            machine,
            machine1,
            earth,
            earth1,
            machine2,
            // ...
        );
        group1.position.x = -60;
    
        const group1Animate = function () { ... };
        return { group1, group1Animate }
    };
    

    最后创建入口文件 main.js

    // main.js
    const gvonte = new Gvo("#canvas-frame", {
        rendererOption: {
            alpha: true,
            antialias: true
        }
    }); // 新建 Gvo 类的实例
    
    groupCommonFn(gvonte); // 初始化分组间公用的部分
    (async function () {
        // 创建分组
        const { group1, group1Animate } = await sceneGroup1Fn(gvonte); // 新建第一个分组
        const { group2, group2Animate } = await sceneGroup2Fn(gvonte); // 新建第二个分组
        const { normalSceneGroup, normalSceneGroupAnimate } = await normalSceneGroupFn(gvonte); // 新建第三个分组
        gvonte.scene.add(group1);
        gvonte.scene.add(group2);
        gvonte.normalScene.add(normalSceneGroup);
    
        // 后期处理
        const { bloomComposer, finalComposer } = createComposer(gvonte);
        document.body.removeChild(document.querySelector("#label1"));
        document.body.removeChild(document.querySelector("#label2"));
    
        // 实现局部辉光的准备工作
        const bloomLayer = gvonte.createLayer(1);
        const materials = {};
    
        // 产生局部辉光的前三步,初始状态必须先调用 bloomComposer.render()
        function readyToBloom() {
            gvonte.scene.traverse(darkenNonBloomed(bloomLayer, materials, Gvo.BlackBasicMaterial));
            bloomComposer.render();
            gvonte.scene.traverse(restoreMaterial(materials));
        }
        readyToBloom();
        gvonte.controls.addEventListener("change", readyToBloom);
    
        function animate() {
            // 管道运动,路线循环流动效果
            group1Animate();
            group2Animate();
            normalSceneGroupAnimate();
    
            // fps监控
            gvonte.stats.update();
    
            // gvonte.renderer.render(gvonte.scene, gvonte.camera);
            finalComposer.render();
            requestAnimationFrame(animate);
        }
        animate();
    })();
    
    展开全文
  • 实现3D机房效果

    2018-06-29 16:44:01
    最美的机房3D效果展示。主要使用three.js创建3D机房模型。
  • 针对webgl的库threejs框架的Web 3D智能数字机房项目实战详细的讲解,随着IT信息技术和移动端的发展,Html5+3D(Webgl)技术已经悄然崛起,3D机房数据中心可视化应用越来越广泛,主要包括3D机房搭建,机柜、服务器、...
  • threejs 室内机房源码

    2021-08-13 15:19:44
    threejs 室内机房源码
  • three.js建立一个可交互的机房机柜

    千次阅读 2020-01-22 17:19:13
    <script src="js/three.js"> <!-- 引入控制器--> <script src="js/OrbitControls_alter.js"> <!-- 引入动画库--> <script src="js/tween.min.js"> // 完整的可以去这里下载 ...
  • 3d机房开源示例

    2017-03-29 17:00:43
    基于vizi框架的3d机房程序,附详细源代码。 使用文档参见相关博客。 初学者,欢迎指正。
  • Three.js呈现3D效果机房--初步方案

    万次阅读 热门讨论 2017-10-25 14:51:15
    3D机房系统是最近用户的需求,通过相关了解最后使用Three.js,也发现最近有东西可以写出来分享: webGL可以让我们在canvas上实现3D效果。而three.js是一款webGL框架,由于其易用性被广泛应用 Three.js是通过对WebGL...
  • } from "three/examples/jsm/controls/DragControls"; //拖拽控件 import { TransformControls } from "three/examples/jsm/controls/TransformControls"; //可视化平移控件 代码 objects 为存储移动的mesh...
  • 使用three.js创建3D机房模型-分享一

    千次阅读 2020-02-08 19:13:59
    使用three.js创建3D机房模型-分享一 序:前段时间公司一次研讨会上,一市场部同事展现了同行业其他公司的3D机房,我司领导觉得这个可以研究研究,为了节约成本,我们在网上大量检索,派本人研究一下web3D的技术,...
  • 针对webgl的库threejs框架的Web 3D智能数字机房项目实战详细的讲解,随着IT信息技术和移动端的发展,Html5+3D(Webgl)技术已经悄然崛起,3D机房数据中心可视化应用越来越广泛,主要包括3D机房搭建,机柜、服务器、...
  • 针对webgl的库threejs框架的Web 3D智能数字机房项目实战详细的讲解,随着IT信息技术和移动端的发展,Html5+3D(Webgl)技术已经悄然崛起,3D机房数据中心可视化应用越来越广泛,主要包括3D机房搭建,机柜、服务器、...
  • 针对webgl的库threejs框架的Web 3D智能数字机房项目实战详细的讲解,随着IT信息技术和移动端的发展,Html5+3D(Webgl)技术已经悄然崛起,3D机房数据中心可视化应用越来越广泛,主要包括3D机房搭建,机柜、服务器、...
  • 1. 引入Threejs 我们可以在public下的index.html文件中引入: 2. 相关依赖文件可以在main.js文件中引入 3. 模型可以放在public文件夹下新建static文件夹放入其中 function loadGlt(obj) { //为模型添加添加天空...
  • 压缩文件包含了一个使用Three.js框架加载obj+mtl模型文件的3d机房实例效果,可用于参考如何使用Three.js加载3dMax之类的软件做出来的obj模型文件以及mtl材质文件。可本地运行(使用火狐浏览器),或者开启本地服务...
  • Web 3D机房,智能数字机房HTML5 WebGL(ThreeJS)匠心打造

    万次阅读 多人点赞 2017-09-05 11:21:19
    在H5使用3D技术门槛比较低了,它的基础是WebGL(ThreeJS),一个OpenGL的浏览器子集,支持大部分主要3D功能接口。目前主流的浏览器都有较好的支持,IE需要11。最近web 3D机房研发告一段落,有时间整理这段时间的一些...
  • ThreeJsTest-3D机房.zip

    2019-10-30 17:09:47
    第一次接触three.js,弄了一个机房的初级demo,可供参考
  • 三维机房(three.js).zip

    2021-05-17 20:19:47
    利用three.js实现三维机房模型,可多种视角查看,并且机柜可点击(开关效果),可参考学习three.js
  • 热力图-基于WebGL/Threejs技术

    千次阅读 2019-05-20 16:34:37
    针对webgl的库threejs框架的热力图功能项目实战详细的讲解,热力图功能在真实项目中应用,主要包括厂区、生产线、机房、库房等实时监控热力分布,建筑或园区人员密集实时监控等综合场景应用。 【课程收益】 threejs...
  • 针对webgl的库threejs框架的热力图功能项目实战详细的讲解,热力图功能在真实项目中应用,主要包括厂区、生产线、机房、库房等实时监控热力分布,建筑或园区人员密集实时监控等综合场景应用。
  • 机房 项目设置 npm install 编译和热重装以进行开发 npm run serve 编译并最小化生产 npm run build 整理和修复文件 npm run lint
  • 1.给threejs中的一个物体说明其作用,设置一个提示标签,代码如下 <div class="absolute bg_rgba6 warning_title_box center px-10 py-10" style="left:1000px;top:345px;"> 火灾警报 </div> 效果...
  • 在模型加消息提示框示例描述与操作指南示例效果展示实现步骤相关接口 示例描述与操作指南 在模型区的正上方显示消息提示,可提示多类信息,本示例默认为“info”类信息提示,还可以选择“success”、“warning”、...

空空如也

空空如也

1 2 3 4 5 ... 19
收藏数 372
精华内容 148
关键字:

threejs机房