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();
    }
}

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

相关推荐
一条晒干的咸魚几秒前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷11 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd79416 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You24 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生36 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
sinat_3842410938 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
菜牙买菜1 小时前
让安卓也能玩出Element-Plus的表格效果
前端