likes
comments
collection
share

three.js 实践3D渲染

作者站长头像
站长
· 阅读数 16

需求分析

因为业务里面需要能够渲染上传后的3D模型数据,本意是需要根据不同的数据格式,支持不同的渲染的,因此想到了使用 three.js 来实现这个功能,本来想直接在网上借鉴一下大佬的内容,因为我们的技术选型是 React ,所以一开始还尝试使用 github 上的一些别人配置好的 react-three 这样的,但是好像没有我需要的那种,只好自己手撸。

实现方式

其实整体的还是根据 three官方文档 上面的参数进行设置,不多废话直接上代码:

  • 引入组件

    import React, { Component, Fragment } from 'react'
    import * as THREE from 'three'
    // import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
    // import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
    import FileService from '@/services/FileService'
    
    import './styles.less'
  • 内容渲染

    class Online3DView extends Component {
      constructor(props) {
        super(props)
        this.state = {
     isModel: false,
     currentName: '暂无名字',
     clientX: 0,
     clientY: 0,
        }
        this.threeRef = React.createRef()
      }
    
      componentDidMount() {
        const { height, width, fileId } = this.props
        let that = this
        // 加载要渲染的文件的数据流(Blob)
        FileService.downloadForPreview(fileId)
     .then((res) => {
       const url = window.URL.createObjectURL(res)
    
       // todo 初始化场景
       const scene = new THREE.Scene()
       // todo 加载相机
       const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 80)
       camera.position.set(1, 25, 25)
       camera.lookAt(new THREE.Vector3(0, 0, 0))
    
       //todo 加载光线
       const ambLight = new THREE.AmbientLight(0x404040, 1)
       const pointLight = new THREE.PointLight(0x404040, 0.8)
       const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
       pointLight.position.set(100, 10, 0)
       pointLight.receiveShadow = true
       scene.add(ambLight)
       scene.add(pointLight)
       scene.add(directionalLight)
    
       //todo  renderer
       const renderer = new THREE.WebGLRenderer({
         antialias: true,
       })
       renderer.setSize(width, height - 10)
       renderer.setClearColor(0xb9d3ff, 1)
    
       // 这里使用哪一种loader就要构建相应的loader,我这里使用了glb文件类型,所以加载了这个
       let glbLoader = new GLTFLoader()
       glbLoader.load(url, function (glTF) {
         glTF.scene.traverse(function (child) {
           if (glTF.isMesh) {
             glTF.frustumCulled = false
             //模型阴影
             glTF.castShadow = true
             //模型自发光
             glTF.material.emissive = glTF.material.color
             glTF.material.emissiveMap = glTF.material.map
           }
         })
    
         scene.add(glTF.scene)
         // todo 场景控制器初始化
         const controls = new OrbitControls(camera, renderer.domElement)
         controls.enabled = true // 鼠标控制是否可用
    
         // 是否自动旋转
         controls.autoRotate = true
         controls.autoRotateSpeed = 0.05
    
         //是否可旋转,旋转速度(鼠标左键)
         controls.enableRotate = true
         controls.rotateSpeed = 0.3
    
         //controls.target = new THREE.Vector();//摄像机聚焦到某一个点
         //最大最小相机移动距离(景深相机)
         controls.minDistance = 10
         controls.maxDistance = 100
    
         //最大仰视角和俯视角
         controls.minPolarAngle = Math.PI / 4 // 45度视角
         controls.maxPolarAngle = Math.PI / 1 // 75度视角
    
         //惯性滑动,滑动大小默认0.25
         controls.enableDamping = true
         controls.dampingFactor = 0.25
    
         //是否可平移,默认移动速度为7px
         controls.enablePan = true
         controls.panSpeed = 0.5
         //controls.screenSpacePanning    = true;
    
         //滚轮缩放控制
         controls.enableZoom = true
         controls.zoomSpeed = 1.5
    
         //水平方向视角限制
         //controls.minAzimuthAngle = -Math.PI/4;
         //controls.maxAzimuthAngle = Math.PI/4;
    
         //todo 绑定到类上
         that.scene = scene
         that.camera = camera
         that.renderer = renderer
         that.controls = controls
         //鼠标移入和移出事件高亮显示选中的模型
         that.currentObjectColor = null //移入模型的颜色
         that.currentObject = null //鼠标移入的模型
    
         // 初始化场景
         // 加载到dom元素上
         that.threeRef.current.appendChild(that.renderer.domElement)
    
         that.start()
         that.resizeFunc1()
         that.resizeFunc2()
       })
     })
     .catch((err) => {})
    
        window.addEventListener('resize', this.resizeFunc1, false)
        window.addEventListener('resize', this.resizeFunc2, false)
      }
    
      componentWillUnmount() {
        this.stop()
        this.renderer && this.threeRef.current.removeChild(this.renderer.domElement)
        window.removeEventListener('resize', this.resizeFunc1, false)
        window.removeEventListener('resize', this.resizeFunc2, false)
      }
    
      // 初始化
      start = () => {
        if (!this.frameId) {
     this.frameId = requestAnimationFrame(this.animate)
        }
      }
    
      // 卸载组件的时候去除
      stop = () => {
        cancelAnimationFrame(this.frameId)
      }
    
      // 更新状态
      animate = () => {
        this.controls.update()
        this.renderScene()
        this.frameId = requestAnimationFrame(this.animate)
      }
    
      renderScene = () => {
        this.renderer.render(this.scene, this.camera)
      }
    
      closeModel = (e) => {
        e.stopPropagation()
        if (this.controls && !this.controls.autoRotate) {
     this.controls.autoRotate = true
        }
        this.setState({
     isModel: false,
        })
      }
    
      resizeFunc1 = () => {
        this.controls.update()
      }
    
      resizeFunc2 = (e) => {
        const dom = document.getElementById('test_three')
        const { offsetWidth, offsetHeight } = dom
        this.camera.aspect = offsetWidth / offsetHeight
        this.camera.updateProjectionMatrix()
        this.renderer.setSize(offsetWidth, offsetHeight)
      }
    
      render() {
        return (
     <Fragment>
       <div
         className={this.props.className || 'three-component'}
         id="test_three"
         ref={this.threeRef}
       />
     </Fragment>
        )
      }
    }
    
  • 使用他

    <Online3DView height={600} width={1150} fileId={record.mediaFileId} />