Vue3 + Three.js + gltf-pipeline大型园区场景渲染与3D业务

在非使用unity 作为3D渲染方案的前提下,对与目前web开发者比较友好的除了canvas场景需要的2D babylon.jsfabric.js, Three.js 是目前针对于jsWeb用户最直接且比较友好的3D引擎方案了。

准备工作:

1.明确需要用的场景方案都有那些,模型需要的加载器是什么

2.模型的场景大小已经相关的交互业务

3.场景的工作环境(浏览器及硬件要求)
step1:

以.glb模型为例

javascript 复制代码
import * as THREE from "three";
import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";

以上就是一个场景绘制需要的基本3个要素
模型压缩

由于建模工程师因为场景规模的原因模型在建立时过多了使用了面,导致整个模型的体积很大,一个校区或者大园区为例,建筑加环境要素及周边地形都整体的模型体积已经达到了100M+,这个时候就需要我们在开发前就考虑模型的压缩问题了
DRACO压缩算法

javascript 复制代码
npm install -g gltf-pipeline
javascript 复制代码
--input, -i                   Path to the glTF or glb file.[string] [required]
--output, -o                  Output path of the glTF or glb file. Separate   
                               resources will be saved to the same directory.  
                                                                       [string]
--binary, -b                  Convert the input glTF to glb.                                      //将输入的glTF转换为glb
                                                     [boolean] [default: false]
--json, -j                    Convert the input glb to glTF.                                      //将输入的glb转换为glTF
                                                     [boolean] [default: false]
--separate, -s                Write separate buffers, shaders, and textures                       //编写单独的缓冲区、着色器和纹理而不是把它们嵌入到glTF中
                               instead of embedding them in the glTF.          
                                                     [boolean] [default: false]
--separateTextures, -t        Write out separate textures only.                                   //只写出单独的纹理
                                                     [boolean] [default: false]
--stats                       Print statistics to console for output glTF                         //将统计信息打印到控制台以输出glTF文件
                               file.                 [boolean] [default: false]     
--keepUnusedElements          Keep unused materials, nodes and meshes.                            //保留未使用的材质、节点和网格
                                                      [boolean] [default: false]
--draco.compressMeshes, -d    Compress the meshes using Draco. Adds the                            //使用Draco压缩网格。添加KHR_draco_mesh_压缩扩展
                                KHR_draco_mesh_compression extension.
                                                      [boolean] [default: false]
--draco.compressionLevel      Draco compression level [0-10], most is 10,                           //Draco压缩级别[0-10],大多数是10,最小值为0。值为0将会连续应用 编码并保留face顺序。
                                least is 0. A value of 0 will apply sequential
                                encoding and preserve face order.
                                                           [number] [default: 7]   
--draco.quantizePositionBits  Quantization bits for position attribute when                        //位置坐标属性的量化位使用Draco压缩。
                                using Draco compression.  [number] [default: 11]

--draco.quantizeNormalBits    Quantization bits for normal attribute when                           //法线属性的量化位使用Draco压缩
                                using Draco compression.   [number] [default: 8]

--draco.quantizeTexcoordBits  Quantization bits for texture coordinate                               //纹理坐标的量化位属性。
                                attribute when using Draco compression.
                                                          [number] [default: 10]

--draco.quantizeColorBits     Quantization bits for color attribute when using                        //使用时颜色属性的量化位德拉科压缩
                                Draco compression.         [number] [default: 8]

--draco.quantizeGenericBits   Quantization bits for skinning attribute (joint                        //蒙皮属性(关节的量化位索引和关节权重)ad自定义属性使用Draco压缩时。
                                indices and joint weights) ad custom attributes
                                when using Draco compression. [number] [default: 8]

--draco.uncompressedFallback  Adds uncompressed fallback versions of the                            //添加未压缩的回退版本压缩网格
                                compressed meshes.    [boolean] [default: false]

  --draco.unifiedQuantization   Quantize positions of all primitives using the            //统一定义的量化网格所有基本体的边界框。 如果这个选项未设置,对每个应用量化原始的可能会导致差距出现在不同图元之间。
                                same quantization grid defined by the unified
                                bounding box of all primitives. If this option
                                is not set, quantization is applied on each
                                primitive separately which can result in gaps
                                appearing between different primitives.
                                                      [boolean] [default: false]


gltf-pipeline的参数有很多这里我们只需要提炼出一个满足我们需要的就够了

javascript 复制代码
gltf-pipeline -i .\public\tep\23.glb -o .\public\tep\23-main.glb  -d --draco.compressionLevel 9 --draco.quantizePositionBits 10 --draco.quantizeColorBits 10
javascript 复制代码
-i .\public\cascl\caa4.glb   //输入路径

-o .\public\cascl\caa4-main.glb  //输出路径及名称

-d --draco.compressionLevel 10 //压缩等级

--draco.quantizePositionBits 20  //量化  0 标识无损压缩 

需要注意的是安装好之后是不可以直接运行的我们需要一个three.js为我们提供的基本依赖draco_decoder.js 这个文件一般就放在node_module/three/examples/js/libs/draco目录下cpoy出来与模型文件一起放在public文件夹下即可

完成上述这些准备工作之后我们开始渲染我们的第一个大园区场景,因为我们使用了压缩算法所以我们需要额外再引入一个解压加载器,并将我们copy出来的draco_decoder.js文件与我们压缩好的模型都放在public下

javascript 复制代码
import {DRACOLoader} from "three/examples/jsm/loaders/DRACOLoader"

step2

初始一个加载模型的方法

javascript 复制代码
export const initMod=(id,filePath,fun)=>{
   container=document.getElementById(id);
   scene = new THREE.Scene();
   camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
   renderer = new THREE.WebGLRenderer({ antialias: true,alpha: true });
  renderer.setSize(container.clientWidth, container.clientHeight);
  container.appendChild(renderer.domElement);
  renderer.setClearColor('#6fc0ec', 1.0);
  renderer.outputEncoding = THREE.sRGBEncoding;
  const loader = new GLTFLoader();
  let dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath("/cascl/"); // 设置public下的解码路径,注意最后面的/
  dracoLoader.setDecoderConfig({ type: "js" });
  dracoLoader.preload();
  loader.setDRACOLoader(dracoLoader);
  loader.load(
    filePath,
    gltf => {
      // 将模型放到中间
      const box = new THREE.Box3().setFromObject(gltf.scene);
      const size = box.getSize(new THREE.Vector3()).length();
      const center = box.getCenter(new THREE.Vector3());
      gltf.scene.position.x -= center.x;
      gltf.scene.position.y -= center.y;
      gltf.scene.position.z -= center.z;
      camera.near = size / 100;
      camera.far = size * 100;
      camera.updateProjectionMatrix();
      camera.position.copy(center);
      camera.position.x += size / 2;
      camera.position.y += size / 2;
      camera.position.z += size / 2;
      camera.lookAt(center);
      scene.add(gltf.scene);
       console.log('---加载的模型',gltf.scene)
      const ambient = new THREE.AmbientLight(0xffffff, 0.4);
      scene.add(ambient);

      //添加在模型的右上角高三倍设置一个光源 太阳
      const light = new THREE.DirectionalLight(0xffffff, 1);
      // 模型宽度
      const width = box.max.x - box.min.x;
      // 模型高度
      const height = box.max.y - box.min.y;
      // 模型深度
      const depth = box.max.z - box.min.z;
      light.position.set(width * 3, height * 3, depth * 3);
      scene.add(light);
// 点光源
      let point = new THREE.PointLight('#74beee',1);
      point.position.set(-width * 3, -height * 3, depth * 3); // 点光源位置
      scene.add(point); // 点光源添加到场景中
      //多设置几个光源
      const light3 = new THREE.DirectionalLight('#8dccee', 1);
      light3.position.set(-width * 3, -height * 3, depth * 3);
      scene.add(light3);
      const light4 = new THREE.HemisphereLight('#8dccee', 0.3);
      scene.add(light4);
      //包含关键帧动画的模型作为参数创建一个播放器
      mixer = new THREE.AnimationMixer(gltf.scene);
      //  获取gltf.animations[0]的第一个clip动画对象
      clipAction = mixer.clipAction(gltf.animations[0]); //创建动画clipAction对象
      clipAction.play(); //播放动画
//不循环播放
      clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
      clipAction.clampWhenFinished = true
      // 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
      clock = new THREE.Clock();
    },
    undefined,
    error => {
      console.error(error);
    }
  );
  camera.position.z = 5;
  // 添加OrbitControls控制器
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;
  controls.screenSpacePanning = false;
  controls.minDistance = 1;
  controls.maxDistance = 1000;
  const tag = labelTag({x: -580,y:50,z: -705});
  tagList.push(tag)
  scene.add(tag);//添加到指定的场景里
  renderLabel()
  animate();
  runLoop()
  renderer.domElement.addEventListener('click', handleModClick, false)
}

上方的初始方案中包含了一个基本的动画加载器由此我们可以完成一个基本的模型加载的场景创建

这是一个可以执行楼层分层爆炸的模型内置的动画由:

javascript 复制代码
   //包含关键帧动画的模型作为参数创建一个播放器
      mixer = new THREE.AnimationMixer(gltf.scene);

完成捕捉及后续播放
step3

接下来我们完成园区的业务需求建设

1.场景需要有天空背景

2.场景需要有关键建筑的标注

3.场景的交互具有高亮环绕

4.场景具有漫游功能

基于以上我们开始增加需要的工具

  1. 场景漫游动画处理库
javascript 复制代码
import TWEEN from '@tweenjs/tween.js';

2.场景天空环境即天空盒

javascript 复制代码
    const urls = [
        '../sky/Above Day B_Cam_3_Right-X.png',//x正方形
        '../sky/Above Day B_Cam_2_Left+X.png',//x负方向
        '../sky/Above Day B_Cam_4_Up+Y.png',//y正方形
        '../sky/Above Day B_Cam_5_Down-Y.png',//y负方向
        '../sky/Above Day B_Cam_0_Front+Z.png',//z正方形
        '../sky/Above Day B_Cam_1_Back-Z.png'//z负方形
    ]
        const textureCube = new THREE.CubeTextureLoader().load(urls)
        scene.background = textureCube

3.场景后处理器

javascript 复制代码
import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer';
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass';
import {OutlinePass} from 'three/examples/jsm/postprocessing/OutlinePass';

在使用了后处理器后因为模型抗拒齿原因我们需要在额外补充一个

javascript 复制代码
import {SMAAPass} from 'three/examples/jsm/postprocessing/SMAAPass';
      //抗锯齿后处理
 const smaaPass = new SMAAPass(container.clientWidth * pixelRatio, container.clientHeight * pixelRatio);

4.一个漫游动画控制的方法

javascript 复制代码
export const createCameraTween = (pos2, pos) => {
    tween = new TWEEN.Tween({
        // 相机开始坐标
        x: camera.position.x,
        y: camera.position.y,
        z: camera.position.z,
        // 相机开始指向的目标观察点
        tx:  current.x,
        ty:   current.y,
        tz:   current.z,
    })
        .to({
            // 相机结束坐标
            x: pos.x,
            y: pos.y,
            z: pos.z,
            // 相机结束指向的目标观察点
            tx: pos.x,
            ty: pos.y,
            tz: pos.z,
        }, 2000)
        .onUpdate(function (obj) {
            // 动态改变相机位置
            camera.position.set(obj.x, obj.y, obj.z);
            // 动态计算相机视线
            camera.lookAt(pos.x, pos.y, -pos.z);
        })
        .start();
    animates();
}
const animates = (time) => {
    TWEEN.update(time);
    requestAnimationFrame(animates);
}

在使用glb/gltf模型中我们也常常会需要处理模型加载发暗,材质渲染失真的情况这里我们也一并加入到初始化的方案内

javascript 复制代码
import {GammaCorrectionShader} from'three/examples/jsm/shaders/GammaCorrectionShader';
import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass';
import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment';

由此我们以及基本完成了所有的加载需要的必备条件即要求,我们渲染一个大园区场景

场景后处理的效果业务由:

javascript 复制代码
const renderOutline = (mod) => {
    if (buildIds.includes(mod.name)) {
        // 创建后处理对象EffectComposer,WebGL渲染器作为参数
        composer = new EffectComposer(renderer);
        renderPass = new RenderPass(scene, camera);
        composer.addPass(renderPass);
// 创建OutlinePass通道
        container = document.getElementById('mod') ? document.getElementById('mod') : document.getElementById('mod2');
        const v2 = new THREE.Vector2(container.clientWidth, container.clientHeight);
        const outlinePass = new OutlinePass(v2, scene, camera);
        // 创建伽马校正通道
        const gammaPass = new ShaderPass(GammaCorrectionShader);
        composer.addPass(gammaPass);
        const pixelRatio = renderer.getPixelRatio()
        //抗锯齿后处理
        const smaaPass = new SMAAPass(container.clientWidth * pixelRatio, container.clientHeight * pixelRatio);
        composer.addPass(smaaPass);
        outlinePass.selectedObjects = [mod];
        outlinePass.visibleEdgeColor.set('#a838ef');
        outlinePass.edgeThickness = 4;
        outlinePass.edgeStrength = 15;
        outlinePass.pulsePeriod = 3;
        composer.addPass(outlinePass);
        animateOutline()
        bus.$emit('showMod', mod)
    } else {
        clearOutline();
    }
}

由于业务延展很多不再过的的赘述,解决方案包含了动态标签切换,标记交互,灯光动态,场景灯光随相机,标签随相机,场景模型过滤,场景模型设备等状态动态更新,单楼层模型,室内模型控制切换即各类物联网设备交互等等。

相关推荐
茶茶只知道学习7 分钟前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css
蒟蒻的贤10 分钟前
Web APIs 第二天
开发语言·前端·javascript
清灵xmf13 分钟前
揭开 Vue 3 中大量使用 ref 的隐藏危机
前端·javascript·vue.js·ref
su1ka11119 分钟前
re题(35)BUUCTF-[FlareOn4]IgniteMe
前端
测试界柠檬20 分钟前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
多多*21 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_8010741522 分钟前
TypeScript异常处理
前端·javascript·typescript
ᅠᅠᅠ@22 分钟前
异常枚举;
开发语言·javascript·ecmascript
小阿飞_23 分钟前
报错合计-1
前端
caperxi24 分钟前
前端开发中的防抖与节流
前端·javascript·html