基于three.js和vue的glb格式3D模型批量加载

发布于 2022-11-25  1288 次阅读


基于three.js和vue的glb格式3D模型批量加载

  前段时间写一个小项目的时候用到了three.js,发现网上的教程很乱很杂学习的很困难,官方文档又不够具体,所以我想写几篇关于three.js的学习经验。

一、GLTF模型加载器的导入

import {
        GLTFLoader
    } from 'three/examples/jsm/loaders/GLTFLoader.js'

  要想使用GLTF模型加载器就得先导入GLTFLoader,这样才能使用方法load方法加载模型。记得在script标签下引入。

二、模型资源放置


  如上图所示,根目录是3d,glb格式的3D模型放在3dmodel目录中,3dmodel目录放在public目录下。这样放置的目的是在路径上无需添加public/,路径格式如下所示。

var address = ['/3dmodel/Landing jet1.glb', '/3dmodel/Landing jet2.glb', '/3dmodel/Landing jet3.glb','/3dmodel/Landing jet4.glb', '/3dmodel/Landing jet5.glb']

三、gltf模型加载器批量加载模型

  glft模型加载器可以加载gltf、glb格式的模型。在第一节将所有模型的地址存放在数组内,只需要一个循环就可以实现对模型的批量加载了。代码如下:

function loadGLTF() {
    let i = 0
    for (i; i < address.length; i++) {
        gltfLoader.load(address[i], (gltf) => {
            let model = gltf.scene
            scene.add(model)//将模型加入到场景中
        })
    }
}

四、模型展示


  上图看上去是一个模型,其实是多个模型的叠加,通过对各个模块位置的调整,来显示一个看上去完整的一个模型。部分模块图如下:

五、其他种类模型加载

[Three.js 中文教程 | 参考手册 | 使用指南 | 动画特效实例 | 踏得网 (techbrood.com)](https://techbrood.com/threejs/docs/)
  搜索Loader,查看其他格式的3D模型的加载器。

六、处理模型的马赛克

function resizeRendererToDisplaySize(renderer) {

   let canvas = renderer.domElement

   var width = window.innerWidth

   var height = window.innerHeight

    var canvasPixelWidth = canvas.width / window.devicePixelRatio

    var canvasPixelHeight = canvas.height / window.devicePixelRatio

    let needResize =canvasPixelWidth !== width || canvasPixelHeight !== height

    if (needResize) {

       renderer.setSize(width, height, false)

    }

    return needResize

}

  如果没有添加以上代码会出现如下图的情况!

七、注意事项及附录

  本文针对于3D模型的批量加载,没有介绍关于camera、scene等内容,建议场景搭建完成后使用。如果想复现所有的功能建议把我的几篇关于three.js的文章看一遍。

    import * as THREE from 'three'
    import {
        GLTFLoader
    } from 'three/examples/jsm/loaders/GLTFLoader.js'
    import {
        OrbitControls
    } from 'three/examples/jsm/controls/OrbitControls'

    export default {
        mounted() {
            this.initThree()
        },
        methods: {
            initThree() {
                addEventListener('click', onClick, false)
                addEventListener('resize', onWindowResize, false)
                const scene = new THREE.Scene()
                scene.background = new THREE.Color('#eee')
                scene.fog = new THREE.Fog('#eee', 20, 100)

                const canvas = document.querySelector('#three')
                const renderer = new THREE.WebGLRenderer({
                    canvas,
                    antialias: true
                })
                renderer.shadowMap.enabled = true

                const camera = new THREE.PerspectiveCamera(
                    50,
                    window.innerWidth / window.innerHeight,
                    0.1,
                    1000
                )
                camera.position.z = 10
                camera.position.x = -10
                camera.position.y = 10
        var address = ['/3dmodel/Landing jet5.glb']
                // var address = ['/3dmodel/Landing jet1.glb'
        //     , '/3dmodel/Landing jet2.glb', '/3dmodel/Landing jet3.glb',
                //  '/3dmodel/Landing jet4.glb', '/3dmodel/Landing jet5.glb'
                // ]
                const gltfLoader = new GLTFLoader()
                const m = 5;
                const n = 19;
                var uuidArray = new Array(m); //存放各个模型中不同组成部分唯一标识id的二维数组
                for (var x = 0; x < m; x++) {
                    uuidArray[x] = new Array(n)
                }
                // var uuidLength = [19, 17, 14, 10, 12]
                var group = new THREE.Group() //将不同的对象组合起来,例如将俩个对象组合起来,设置一个位置,俩个对象的位置都发生改变
                function loadGLTF() {
                    let i = 0
          let j = 0
                    for (i; i < address.length; i++) {
                        let array = []
                        gltfLoader.load(address[i], (gltf) => {
              j++
              console.log("分隔")
                            let model = gltf.scene
                            // 遍历模型每部分
                            model.traverse((o) => {
                console.log(o)//输出模型的组成部分
                                array.push(o.uuid)
                                //将图片作为纹理加载
                                let explosionTexture = new THREE.TextureLoader().load(
                                    '/3dmodel/textures/darkblue.jpg'
                                )
                                //调整纹理图的方向
                                explosionTexture.flipY = false
                                //将纹理图生成基础网格材质(MeshBasicMaterial)
                                let material = new THREE.MeshBasicMaterial({
                                    map: explosionTexture,
                                })
                                // //给模型每部分上材质
                  o.material = material

                                if (o.isMesh) {
                                    o.castShadow = true
                                    o.receiveShadow = true
                                }
                            })
                            scene.add(model)
                        })
                        uuidArray[i] = array
                    }
                }

                loadGLTF()
                //加载GLFT及GLB模型

                const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6)
                hemLight.position.set(0, 48, 0)
                scene.add(hemLight)

                const dirLight = new THREE.DirectionalLight(0xffffff, 0.6)
                //光源等位置
                dirLight.position.set(-10, 8, -5)
                //可以产生阴影
                dirLight.castShadow = true
                dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024)
                scene.add(dirLight)

                let floorGeometry = new THREE.PlaneGeometry(8000, 8000)
                let floorMaterial = new THREE.MeshPhongMaterial({
                    color: 0x857ebb,
                    shininess: 0,
                })

                let floor = new THREE.Mesh(floorGeometry, floorMaterial)
                floor.rotation.x = -0.5 * Math.PI
                floor.receiveShadow = true
                floor.position.y = -3
                scene.add(floor)

                const controls = new OrbitControls(camera, renderer.domElement)
                controls.enableDamping = true

                function onClick(event) {
                    let selectObject
                    //获取raycaster和所有模型相交的数组,其中的元素按照距离排序,越近的越靠前
                    let intersects = getIntersects(event)

                    //获取选中最近的Mesh对象
                    //instance坐标是对象,右边是类,判断对象是不是属于这个类的
                    if (intersects.length !== 0 && intersects[0].object.type === 'Mesh') {
                        // intersects[0].object.material.color.set(0x00FF00)
                        //console.log(intersects[0].object.material.color);
                        selectObject = intersects[0].object
                        let num = judgeModule(selectObject.uuid)

                        if (num[0] > 0) { //接口,判断点击了哪个子系统的哪个部件
                            console.log("选中了第" + num[0] + "个子系统" + "的第" + num[1] + "部件")
                        }
                        changeMaterial(selectObject)
                        // console.log(judgeClick(intersects[0].object.parent.parent.parent))
                    } else {
                        console.log('未选中 Mesh!')
                    }
                }

                function judgeModule(uuid) {
                    let num = [-1, -1]
                    let i, j
                    for (i = 0; i < uuidArray.length; i++) {
                        for (j = 0; j < uuidArray[i].length; j++) {
                            if (uuidArray[i][j] == uuid) { //妈的双等于写成单等于了,找半天没找到
                                num[0] = i + 1 //返回子系统的编号,数组下标加一是编号
                                num[1] = j + 1 //返回子系统中部件的编号
                            }
                        }
                    }
                    return num //代表没有点中子系统
                }

                function getIntersects(event) {
                    event.preventDefault()
                    // 阻止默认的点击事件执行, https://developer.mozilla.org/zh-CN/docs/Web/API/Event/preventDefault
                    //console.log("event.clientX:" + event.clientX);
                    //console.log("event.clientY:" + event.clientY);

                    //声明 rayCaster 和 mouse 变量
                    let rayCaster = new THREE.Raycaster()
                    let mouse = new THREE.Vector2()

                    //通过鼠标点击位置,计算出raycaster所需点的位置,以屏幕为中心点,范围-1到1
                    mouse.x = ((event.clientX - canvas.getBoundingClientRect().left) / canvas.offsetWidth) * 2 - 1
                    mouse.y = -((event.clientY - canvas.getBoundingClientRect().top) / canvas.offsetHeight) * 2 + 1

                    //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
                    rayCaster.setFromCamera(mouse, camera)

                    //获取与射线相交的对象数组, 其中的元素按照距离排序,越近的越靠前。
                    //+true,是对其后代进行查找,这个在这里必须加,因为模型是由很多部分组成的,后代非常多。
                    let intersects = rayCaster.intersectObjects(scene.children, true)
                    //返回选中的对象
                    return intersects
                }

                function onWindowResize() {
                    camera.aspect = window.innerWidth / window.innerHeight
                    camera.updateProjectionMatrix()
                    renderer.setSize(window.innerWidth, window.innerHeight)
                }

                function changeMaterial(object) {
                    let material = new THREE.MeshLambertMaterial({
                        color: 0xffffff * Math.random(),
                        transparent: object.material.transparent ? false : true,
                        opacity: 0.8
                    });
                    object.material = material;
                }

                function animate() {
                    controls.update()
                    renderer.render(scene, camera)
                    requestAnimationFrame(animate)

                    if (resizeRendererToDisplaySize(renderer)) {
                        let canvas = renderer.domElement
                        camera.aspect = canvas.clientWidth / canvas.clientHeight
                        camera.updateProjectionMatrix()
                    }
                }
                animate()

                function resizeRendererToDisplaySize(renderer) {
                    let canvas = renderer.domElement
                    var width = window.innerWidth
                    var height = window.innerHeight
                    var canvasPixelWidth = canvas.width / window.devicePixelRatio
                    var canvasPixelHeight = canvas.height / window.devicePixelRatio

                    let needResize =
                        canvasPixelWidth !== width || canvasPixelHeight !== height
                    if (needResize) {
                        renderer.setSize(width, height, false)
                    }
                    return needResize
                }
            },
        },
    }

学习记录,经验分享