上传图片转成3D VR效果 / 用photo-sphere-viewer实现图片VR效果

发布于:2025-05-29 ⋅ 阅读:(26) ⋅ 点赞:(0)

系统简介 : 该系统为 react + TS + tailwindcss + photo-sphere-viewer 的响应式 VR360 项目, 上传图片后可实现手动旋转 3D 图片,还包含了 6 贴图立方体展示和 6 贴图动态展示

目前为单图切换模式

全部页面概览

这是单面VR页面的代码(gif展示页面)

import React, { useRef, useState, useEffect } from 'react'
import { Viewer } from 'photo-sphere-viewer'
import 'photo-sphere-viewer/dist/photo-sphere-viewer.css'

import { PlusOutlined } from '@ant-design/icons'
import { Image, Upload } from 'antd'
import type { GetProp, UploadFile, UploadProps } from 'antd'

type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0]

const getBase64 = (file: FileType): Promise<string> =>
  new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => resolve(reader.result as string)
    reader.onerror = error => reject(error)
  })

export default function VrUploader () {
  const [selectedFileUrl, setSelectedFileUrl] = useState('')
  const [confirmedImageUrl, setConfirmedImageUrl] = useState('')
  const viewerRef = useRef<HTMLDivElement | null>(null)
  const viewerInstance = useRef<Viewer | null>(null)

  const [previewOpen, setPreviewOpen] = useState(false)
  const [previewImage, setPreviewImage] = useState('')
  const [fileList, setFileList] = useState<UploadFile[]>([])

  const handlePreview = async (file: UploadFile) => {
    if (!file.url && !file.preview) {
      file.preview = await getBase64(file.originFileObj as FileType)
    }

    setPreviewImage(file.url || (file.preview as string))
    setPreviewOpen(true)
  }

  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
    // 始终只保留最后一张图片
    const latestFileList = newFileList.slice(-1)
    setFileList(latestFileList)

    if (latestFileList.length > 0) {
      const file = latestFileList[0].originFileObj as File
      const url = URL.createObjectURL(file)
      setSelectedFileUrl(url)
    } else {
      setSelectedFileUrl('')
      setConfirmedImageUrl('')
    }
  }
  const uploadButton = (
    <button style={{ border: 0, background: 'none' }} type='button'>
      <PlusOutlined />
      <div style={{ marginTop: 8 }}>上传</div>
    </button>
  )

  const handleConfirm = () => {
    if (selectedFileUrl) {
      setConfirmedImageUrl(selectedFileUrl)
    }
  }

  const handleCancel = () => {
    setFileList([])
    setSelectedFileUrl('')
    setConfirmedImageUrl('')
    // 销毁 Viewer 实例并清空引用
    if (viewerInstance.current) {
      viewerInstance.current.destroy()
      viewerInstance.current = null
    }
  }

  const [isRetote, setIsRetote] = useState(false) // 是否旋转

  const handleRetote = () => {
    setIsRetote(prev => {
      const newState = !prev

      if (viewerInstance.current) {
        if (newState) {
          viewerInstance.current.startAutorotate()
        } else {
          viewerInstance.current.stopAutorotate()
        }
      }

      return newState
    })
  }

  useEffect(() => {
    if (confirmedImageUrl && viewerRef.current) {
      if (viewerInstance.current) {
        viewerInstance.current.setPanorama(confirmedImageUrl)
      } else {
        viewerInstance.current = new Viewer({
          container: viewerRef.current, //必---html元素信息
          panorama: confirmedImageUrl, //必---图片路径
          description: '<p>This is a description.</p>'
        })
      }
    }
  }, [confirmedImageUrl])

  return (
    <div className='p-4 cursor-pointer'>
      <div className='bg-gray-100 flex items-center justify-center w-100'>
        <div className='min-w-[70%]'>
          <h2 className='text-2xl font-bold mb-4 mt-4'>
            <p>单图上传</p>
            <p>VR 生成(photo-sphere-viewer)</p>
          </h2>
          <div className='grid grid-cols-2 md:grid-cols-3 gap-4 mb-4'>
            <Upload
              action='https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload'
              listType='picture-card'
              fileList={fileList}
              onPreview={handlePreview}
              onChange={handleChange}
            >
              {fileList.length >= 8 ? null : uploadButton}
            </Upload>
            {previewImage && (
              <Image
                wrapperStyle={{ display: 'none' }}
                preview={{
                  visible: previewOpen,
                  onVisibleChange: visible => setPreviewOpen(visible),
                  afterOpenChange: visible => !visible && setPreviewImage('')
                }}
                src={previewImage}
              />
            )}
          </div>

          {/* 按钮区域 */}
          <div className='flex w-full justify-between'>
            <button
              onClick={handleConfirm}
              disabled={!selectedFileUrl}
              className={`px-4 py-2 rounded-md text-white ${
                selectedFileUrl
                  ? 'bg-blue-600 hover:bg-blue-700'
                  : 'bg-gray-400 cursor-not-allowed'
              }`}
            >
              生成 VR
            </button>

            <button
              onClick={handleRetote}
              className={`px-4 py-2 rounded-md text-white bg-green-600 hover:bg-green-700`}
            >
              {!isRetote ? '开始巡检' : '停止巡检'}
            </button>
            <button
              onClick={handleCancel}
              className={`px-4 py-2 rounded-md text-white bg-red-600 hover:bg-red-700`}
            >
              重置图片
            </button>
          </div>

          {/* VR区域 */}
          <div
            className='h-[50vh] w-full bg-black mt-6 rounded overflow-hidden mb-6'
            ref={viewerRef}
          >
            {!selectedFileUrl && (
              <p className='text-white p-4'>请上传一张比例为2:1 / 圆形图</p>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}

完整代码 : GitHub - hyy20000804/Img-to-VR: 响应式图片转VR系统


网站公告

今日签到

点亮在社区的每一天
去签到