THREEJS可遮挡CSS3D标签

113 阅读2分钟

介绍:css3D标签作为three中最重要的部分之一,却无法实现3D空间的有效遮挡顺序。本文参考babylonjs的源代码实现了threejs的可遮挡css3D标签,具体思路是用利用材质的noblending特性作为遮罩,使得下层的css3Ddom变成可见。



源代码:
import {
  Matrix4,
  Object3D,
  Quaternion,
  Vector3,
  Mesh,
  PlaneGeometry,
  MeshBasicMaterial,
  NoBlending,
  DoubleSide,
  Color
} from 'three'

/**
 * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs
 */

const _position = new Vector3()
const _quaternion = new Quaternion()
const _scale = new Vector3()

class JCSS3DObject extends Object3D {
  constructor(element = document.createElement('div')) {
    super()

    this.isJCSS3DObject = true

    this.element = element
    this.element.style.position = 'absolute'
    this.element.style.pointerEvents = 'auto'
    this.element.style.userSelect = 'none'

    const width = Number(this.element.style.width.replace('px', ''))
    const height = Number(this.element.style.height.replace('px', ''))
        console.log(width,height)
    const maskGeo = new PlaneGeometry(width,height)
    const maskMaterial = new MeshBasicMaterial({
        transparent:true,
        opacity:0,
        blending:NoBlending,
        color:new Color(0x000000),
        side:DoubleSide
    })
    const mask = new Mesh(maskGeo,maskMaterial)
    this.add(mask)

    this.element.setAttribute('draggable', false)

    this.addEventListener('removed', function() {
      this.traverse(function(object) {
        if (
          object.element instanceof object.element.ownerDocument.defaultView.Element &&
					object.element.parentNode !== null
        ) {
          object.element.remove()
        }
      })
    })
  }

  copy(source, recursive) {
    super.copy(source, recursive)

    this.element = source.element.cloneNode(true)

    return this
  }
}

class JCSS3DSprite extends JCSS3DObject {
  constructor(element) {
    super(element)

    this.isJCSS3DSprite = true

    this.rotation2D = 0
  }

  copy(source, recursive) {
    super.copy(source, recursive)

    this.rotation2D = source.rotation2D

    return this
  }
}

//

const _matrix = new Matrix4()
const _matrix2 = new Matrix4()

class JCSS3DRenderer {
  constructor(parameters = {}) {
    const _this = this

    let _width, _height
    let _widthHalf, _heightHalf

    const cache = {
      camera: { style: '' },
      objects: new WeakMap()
    }

    const domElement = parameters.element !== undefined ? parameters.element : document.createElement('div')

    domElement.style.overflow = 'hidden'
    domElement.style.position = 'absolute'
    domElement.style.top = 0
    domElement.style.zIndex = -1

    this.domElement = domElement

    const viewElement = document.createElement('div')
    viewElement.style.transformOrigin = '0 0'
    viewElement.style.pointerEvents = 'none'
    domElement.appendChild(viewElement)

    const cameraElement = document.createElement('div')

    cameraElement.style.transformStyle = 'preserve-3d'

    viewElement.appendChild(cameraElement)

    this.getSize = function() {
      return {
        width: _width,
        height: _height
      }
    }

    this.render = function(scene, camera) {
      const fov = camera.projectionMatrix.elements[ 5 ] * _heightHalf

      if (camera.view && camera.view.enabled) {
        // view offset
        viewElement.style.transform = `translate( ${-camera.view.offsetX * (_width / camera.view.width)}px, ${-camera.view.offsetY * (_height / camera.view.height)}px )`

        // view fullWidth and fullHeight, view width and height
        viewElement.style.transform += `scale( ${camera.view.fullWidth / camera.view.width}, ${camera.view.fullHeight / camera.view.height} )`
      } else {
        viewElement.style.transform = ''
      }

      if (scene.matrixWorldAutoUpdate === true) scene.updateMatrixWorld()
      if (camera.parent === null && camera.matrixWorldAutoUpdate === true) camera.updateMatrixWorld()

      let tx, ty

      if (camera.isOrthographicCamera) {
        tx = -(camera.right + camera.left) / 2
        ty = (camera.top + camera.bottom) / 2
      }

      const scaleByViewOffset = camera.view && camera.view.enabled ? camera.view.height / camera.view.fullHeight : 1
      const cameraCSSMatrix = camera.isOrthographicCamera
        ? `scale( ${scaleByViewOffset} )` + 'scale(' + fov + ')' + 'translate(' + epsilon(tx) + 'px,' + epsilon(ty) + 'px)' + getCameraCSSMatrix(camera.matrixWorldInverse)
        : `scale( ${scaleByViewOffset} )` + 'translateZ(' + fov + 'px)' + getCameraCSSMatrix(camera.matrixWorldInverse)
      const perspective = camera.isPerspectiveCamera ? 'perspective(' + fov + 'px) ' : ''

      const style = perspective + cameraCSSMatrix +
				'translate(' + _widthHalf + 'px,' + _heightHalf + 'px)'

      if (cache.camera.style !== style) {
        cameraElement.style.transform = style

        cache.camera.style = style
      }

      renderObject(scene, scene, camera, cameraCSSMatrix)
    }

    this.setSize = function(width, height) {
      _width = width
      _height = height
      _widthHalf = _width / 2
      _heightHalf = _height / 2

      domElement.style.width = width + 'px'
      domElement.style.height = height + 'px'

      viewElement.style.width = width + 'px'
      viewElement.style.height = height + 'px'

      cameraElement.style.width = width + 'px'
      cameraElement.style.height = height + 'px'
    }

    function epsilon(value) {
      return Math.abs(value) < 1e-10 ? 0 : value
    }

    function getCameraCSSMatrix(matrix) {
      const elements = matrix.elements

      return 'matrix3d(' +
				epsilon(elements[ 0 ]) + ',' +
				epsilon(-elements[ 1 ]) + ',' +
				epsilon(elements[ 2 ]) + ',' +
				epsilon(elements[ 3 ]) + ',' +
				epsilon(elements[ 4 ]) + ',' +
				epsilon(-elements[ 5 ]) + ',' +
				epsilon(elements[ 6 ]) + ',' +
				epsilon(elements[ 7 ]) + ',' +
				epsilon(elements[ 8 ]) + ',' +
				epsilon(-elements[ 9 ]) + ',' +
				epsilon(elements[ 10 ]) + ',' +
				epsilon(elements[ 11 ]) + ',' +
				epsilon(elements[ 12 ]) + ',' +
				epsilon(-elements[ 13 ]) + ',' +
				epsilon(elements[ 14 ]) + ',' +
				epsilon(elements[ 15 ]) +
			')'
    }

    function getObjectCSSMatrix(matrix) {
      const elements = matrix.elements
      const matrix3d = 'matrix3d(' +
				epsilon(elements[ 0 ]) + ',' +
				epsilon(elements[ 1 ]) + ',' +
				epsilon(elements[ 2 ]) + ',' +
				epsilon(elements[ 3 ]) + ',' +
				epsilon(-elements[ 4 ]) + ',' +
				epsilon(-elements[ 5 ]) + ',' +
				epsilon(-elements[ 6 ]) + ',' +
				epsilon(-elements[ 7 ]) + ',' +
				epsilon(elements[ 8 ]) + ',' +
				epsilon(elements[ 9 ]) + ',' +
				epsilon(elements[ 10 ]) + ',' +
				epsilon(elements[ 11 ]) + ',' +
				epsilon(elements[ 12 ]) + ',' +
				epsilon(elements[ 13 ]) + ',' +
				epsilon(elements[ 14 ]) + ',' +
				epsilon(elements[ 15 ]) +
			')'

      return 'translate(-50%,-50%)' + matrix3d
    }

    function hideObject(object) {
      if (object.isCSS3DObject) object.element.style.display = 'none'

      for (let i = 0, l = object.children.length; i < l; i++) {
        hideObject(object.children[ i ])
      }
    }

    function renderObject(object, scene, camera, cameraCSSMatrix) {
      if (object.visible === false) {
        hideObject(object)

        return
      }

      if (object.isJCSS3DObject) {
        const visible = (object.layers.test(camera.layers) === true)

        const element = object.element

        element.style.display = visible === true ? '' : 'none'

        if (visible === true) {
          object.onBeforeRender(_this, scene, camera)

          let style

          if (object.isJCSS3DSprite) {
            // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/

            _matrix.copy(camera.matrixWorldInverse)
            _matrix.transpose()

            if (object.rotation2D !== 0) _matrix.multiply(_matrix2.makeRotationZ(object.rotation2D))

            object.matrixWorld.decompose(_position, _quaternion, _scale)
            _matrix.setPosition(_position)
            _matrix.scale(_scale)

            _matrix.elements[ 3 ] = 0
            _matrix.elements[ 7 ] = 0
            _matrix.elements[ 11 ] = 0
            _matrix.elements[ 15 ] = 1

            style = getObjectCSSMatrix(_matrix)
          } else {
            style = getObjectCSSMatrix(object.matrixWorld)
          }

          const cachedObject = cache.objects.get(object)

          if (cachedObject === undefined || cachedObject.style !== style) {
            element.style.transform = style

            const objectData = { style: style }
            cache.objects.set(object, objectData)
          }

          if (element.parentNode !== cameraElement) {
            cameraElement.appendChild(element)
          }

          object.onAfterRender(_this, scene, camera)
        }
      }

      for (let i = 0, l = object.children.length; i < l; i++) {
        renderObject(object.children[ i ], scene, camera, cameraCSSMatrix)
      }
    }
  }
}

export { JCSS3DObject, JCSS3DSprite, JCSS3DRenderer }

使用:

import  { JCSS3DObject, JCSS3DSprite, JCSS3DRenderer } from 'JCSS3DRenderer'

标签渲染器挂载dom元素
this.cssRenderer = new JCSS3DRenderer();
this.cssRenderer.setSize(window.innerWidth, window.innerHeight);
this.cssRenderer.domElement.style.position = 'absolute';
this.cssRenderer.domElement.style.top = 0;
this.cssRenderer.domElement.style.zIndex = -1;
document.body.appendChild(this.cssRenderer.domElement);

添加css3ddbject或者css3dspite
const element = document.getElementById('text')
const cssObject = new JCSS3DObject(element);
this.scene.add(cssObject);

帧渲染
function animate () {
    renderer.render(scene, camera);
    cssRenderer.render(scene, camera);
}
render.setAnimationLoop(animate)