从零开发一个全景VR(Vue3+Vite+THREEjs)框架搭建

660 阅读5分钟

从零开发一个全景VR(Vue3+Vite+THREEjs)框架搭建

一、 如何创建Vue3项目自行百度(本次只提供如何来实现全景VR展示思路逻辑)

  1. 利用Vue3Hooks(之前使用的是Vue2+class类)来搭建
  2. 导包有threejs swiper vue-awesome-swiper(用于展示每个场景 可选) sass tweenjs(实现threejs动画) lodash

二、代码部分

  • useThree(hooks部分代码)
// Threejs核心主体+css渲染器
let scene = shallowRef(null) // 场景
let camera = shallowRef(null);// 相机
let renderer = shallowRef(null);// 渲染器
let cssRenderer = shallowRef(null);// css渲染器
// 用于旋转视角的控制器
let orbitControls = shallowRef(null);// 控制器
// 用于处理在某个阶段以及功能需要触发的事件功能
const renderMixins = new Map();// 混入自定义方法
const loader = new Loader(); // 加载所有threejsLoader (本次只使用了CubeTextureLoader 用于加载图片贴图)
const sceneContainer = new THREE.Group() // 场景容器
const spriteContainer = new THREE.Group() // 精灵容器
// 以下代码是用于dev环境测试帧率以及性能状态
const gui = null;
gui = import.meta.env.VITE_ENV !== 'dev' ? '' : new GUI();// 编辑器
const stats = null
stats = import.meta.env.VITE_ENV !== 'dev' ? '' : new STATS();
stats && (stats.domElement.style.position = 'absolute', stats.domElement.style.left = null, stats.domElement.style.right = '10px')
stats && document.querySelector('body').appendChild(stats.domElement);
gui && (gui.domElement.style.right = '0', gui.domElement.style.left = '15px')

// init初始化场景 相机 渲染器
    const _initScene = () => {
        const scene = new THREE.Scene()
        return scene
    }
    const _initCamera = (element) => {
        const fov = 20
        const near = 0.1
        const far = 2000
        const aspect = element.offsetWidth / element.offsetHeight
        const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
        camera.position.set(0, 0, 0)
        return camera
    }
    const _initCSSRenderer = (element) => {
        const CSSRenderer = new CSS2DRenderer()
        CSSRenderer.setSize(element.offsetWidth, element.offsetHeight)
        CSSRenderer.domElement.style.position = 'absolute'
        CSSRenderer.domElement.style.left = '0px'
        CSSRenderer.domElement.style.top = '0px'
        element.appendChild(CSSRenderer.domElement)
        return CSSRenderer
    }
    const _initRenderer = (element) => {
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.domElement.id = 'mainCanvas'
        renderer.domElement.style.position = 'absolute'
        renderer.domElement.style.top = '0px'
        renderer.domElement.style.zIndex = '1'
        renderer.outputColorSpace = THREE.SRGBColorSpace
        renderer.shadowMap.enabled = true
        renderer.setSize(element.offsetWidth, element.offsetHeight)
        renderer.localClippingEnabled = true
        renderer.toneMapping = THREE.ACESFilmicToneMapping
        element.appendChild(renderer.domElement)
        return renderer
    }
    const _initOrbitControls = (camera, domElement) => {
        const orbitControls = new OrbitControls(camera, domElement)
        orbitControls.rotateSpeed *= -.25;
        orbitControls.enableDamping = true;
        orbitControls.enableZoom = false
        orbitControls.target = new THREE.Vector3(0, 0, 0)
        orbitControls.update()
        return orbitControls
    }
    const _initResponsiveResize = () => {
        window.addEventListener('resize', () => {
            camera.value.aspect = baseScene.value.offsetWidth / baseScene.value.offsetHeight
            camera.value.updateProjectionMatrix()
            renderer.value.setSize(baseScene.value.offsetWidth, baseScene.value.offsetHeight)
            cssRenderer.value.setSize(baseScene.value.offsetWidth, baseScene.value.offsetHeight)
        })
    }
    const render = () => {
        if (!renderer.value) return;
        renderer.value.render(scene.value, camera.value)
        renderMixins.forEach((mixin) => isFunction(mixin) && mixin())
        cssRenderer.value.render(scene.value, camera.value)
        orbitControls.value.update();
        TWEEN.update()
        stats?.update()
        requestAnimationFrame(() => render())
    }
     // 创建球体
    const createSphere = (options) => {
        const { side, cubeTexture } = options
        const onCompleted = (texture, resolve) => resolve(texture)
        return new Promise((resolve) => {
               // map 这里我使用着色器 用于实现切换场景动画(不提供)
               // 如果不使用着色器 MeshBasicMaterial材质等其他材质
                const sphere = new THREE.Mesh(
                    new THREE.SphereGeometry(1, 32, 32),
                    new THREE.ShaderMaterial({
                        uniforms: {
                            tCube: {
                                value: map
                            },
                            blurAmount: {
                                value: 0
                            }
                        },
                        // 双面
                        side: side,
                        vertexShader,
                        fragmentShader
                    })
                )
                sphere.name = '全景容器'
                onCompleted(sphere, resolve)
            })
        })
    }
    onMounted(() => {
        const $dom = baseScene.value = document.querySelector('#baseScene')
        scene.value = _initScene()
        scene.value.add(spriteContainer, sceneContainer)
        camera.value = _initCamera($dom)
        renderer.value = _initRenderer($dom)
        cssRenderer.value = _initCSSRenderer($dom)
        orbitControls.value = _initOrbitControls(camera.value, renderer.value.domElement)
        _initResponsiveResize()
    })
    return {
    ...
    }
    ...
  • Loader(Class代码 迁移于Vue2)
class Loader {
    static cube_texture_loader
    constructor() {
        this.cube_texture_loader = new THREE.CubeTextureLoader()
        ... 
    }
}
  • 调用入口(hooks部分)
// 初始化球体 也可以使用正方体
    const initSphere = (name) => {
        return new Promise(async (resolve, reject) => {
        // 图片路径 可以写静态或者加载 (这里之后可能会改成相机缩放或者移动到某个角度去加载当前方向照片 目前是统一加载 图片素材较小情况下)
            const texture = [
                `scenes/${name}/px.png`,
                `scenes/${name}/nx.png`,
                `scenes/${name}/py.png`,
                `scenes/${name}/ny.png`,
                `scenes/${name}/pz.png`,
                `scenes/${name}/nz.png`,
            ]
            await createSphere({
                cubeTexture: texture,
                side: THREE.DoubleSide
            }).then((sphere) => {
                sceneContainer.add(sphere)
                resolve()
            })

        })
    }
  // 切换场景(代码为我纯手写 我本内部代码已经有编辑器[精灵部分为低代码部分 可操作性高 实现起来方便])
    const createSprite = () => {
     // 创建dom
     const $dom = ....
     // 精灵部分
     const sprite = new CSS2DObject($dom)
     sprite.position.set(0,0,1) // 坑(z轴必须为1 因为球的半径为1 这个跟球的半径有关系 如果跟半径相差过大 旋转场景的时候 精灵相对移动)
     spriteContainer.add(sprite)
    }
    onMounted(async () => {
        await initSphere(res).then(() => {
            console.debug('球体容器完成');
        })
        camera.value.position.set(0, 0, -.5) // 不能设置与控制器一样的位置  控制器会失效 z轴-.5是最好的角度
        orbitControls.value.update()
        render() // 所有加载完成后开始渲染
    })

总结

目前实现的功能(时间有限 目前先暂停 在研究OpenCV(Python)合并全景VR图片 如果各位大佬会这方面的东西 欢迎讨论!): 以下功能实现为编辑器内

  1. 点击切换场景
  2. 配置:

1. 基础配置

  • 标题设置
  • 首次场景(第一次进来是哪个场景)

2. 场景配置

前提: public/scenes 目录内必须提前放入该场景的贴图文件 添加场景按钮 添加一个场景 可输入场景名字 对应该场景目录内文件夹名字 是否展示 用于前台展示时候是否显示该场景 添加精灵 添加该场景内容

  1. 精灵名字(该精灵叫什么)
  2. 精灵类型(HTML标签 用于生成什么dom)
  3. 添加样式 (css样式 用于添加该dom的css样式 (如果是图片 视频 等多媒体 src 也写入样式中))
  4. 添加精灵事件(该精灵触发什么事件 类型(目前只有切换场景))
  5. 精灵位置(xy 在场景中哪个位置 可移动)
  6. 精灵内部可以添加内部精灵
  7. 目前只能添加精灵类型 (HTML标签)
  8. 添加样式 与下面动画相结合(css样式)(如果是图片 视频 等多媒体 src 也写入样式中)
  9. 添加动画(css动画) 名字 0% 100% 对应该帧动画的高度

*内置精灵 在public/builtInSprites.json 文件内*

*用于配置内置精灵 格式在/public/remead.md 文件内*

未解决问题

  • 1. 视角跟随移动 (camera视角调了但是orbitControls的视角未更改 导致出问题)
  • 2. 目前提交后下载出文件而不是直接更改json(要么通过node服务器来更改 之后部署得部署两次)
  • 3. 精灵大小修改后无效(因为是css2DObject 缩放无用)
  • 4. css样式修改后场景内动态更新
  • 5. 场景未保存 再次添加精灵 之前精灵位置重置(归于问题2)
  • 6. 缺少每次切换场景后 视角是否重置问题 (归于问题1)
  • 8. 目前只有一层,之后渲染要分瓦片来渲染改图层
  • 9. ...

最后希望大家多多支持+三连!!!