Three.js CSS2DRenderer

1,735 阅读4分钟

CSS2DRenderer

这是一个2D的render,可以在页面中渲染出一个div标签。下图中的城市名称就是渲染出来的,所以地图旋转的时候文字依然能够保持直立和水平。很实用的一个功能。

在这里插入图片描述

    let laberDiv = document.createElement('div');//创建div容器
    laberDiv.className = 'label';
    laberDiv.textContent = cityname;
    laberDiv.style.marginTop = '-1em';
    let pointLabel = new CSS2DObject(laberDiv);   
    pointLabel.position.set(x, y,z);    
    scene.add(pointLabel);

    labelRenderer = new CSS2DRenderer(); //新建CSS2DRenderer 
    labelRenderer.setSize(divWidth, divHeight);
    labelRenderer.domElement.style.position = 'absolute';
    labelRenderer.domElement.style.top = 0;
    document.getElementById(webglDivId).appendChild(labelRenderer.domElement);

 	controls = new OrbitControls(camera); //加renderer.domElement,会不灵光
	renderer.render(scene, camera);
    labelRenderer.render( scene, camera );//渲染

源码,简单的修改了一下使能够import { CSS2DRenderer, CSS2DObject } from "CSS2DRenderer",用在webpack开发的项目中。主要把类似 THREE.CSS2DObject的改成 var CSS2DObject等修改。在调用的时候前面也不需要加THREE和控制器一起使用的时候有个问题。页面中共有Renderer,labelRenderer 两个randerer。一个渲染出canvas,一个是div。大小相同,位置重叠,div会在canvas上面显示。所以在(camera,renderer.domElement)中,renderer.domElement会在labelRenderer.domElement下面而导致 controls无法触发。所以要把这个地方换成labelRenderer.domElement或者不写。不写默认是document,会在整个页面触发控制事件,单页满屏的时候没关系,在窗口模式的时候就出现了不理想的情况。 使用了CSS2DRenderer之后,在重置窗口的时候也需要重置一下。

源码研究一下:

/**
 * @author mrdoob / http://mrdoob.com/
 */
import * as THREE from "three"
let CSS2DObject = function ( element ) {
	THREE.Object3D.call( this );
	this.element = element;
	this.element.style.position = 'absolute';
	this.addEventListener( 'removed', function ( event ) {
		if ( this.element.parentNode !== null ) {
			this.element.parentNode.removeChild( this.element );
		}
	} );
};

CSS2DObject.prototype = Object.create( THREE.Object3D.prototype );
CSS2DObject.prototype.constructor = CSS2DObject;

let CSS2DRenderer = function () {
	console.log( 'CSS2DRenderer', THREE.REVISION );
	var _width, _height;
	var _widthHalf, _heightHalf;
	var vector = new THREE.Vector3();
	var viewMatrix = new THREE.Matrix4();
	var viewProjectionMatrix = new THREE.Matrix4();

	var cache = {
		objects: new WeakMap()
	};

	var domElement = document.createElement( 'div' );
	domElement.style.overflow = 'hidden';
	this.domElement = domElement;

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

	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';

	};

	var renderObject = function ( object, camera ) {
		if ( object instanceof CSS2DObject ) {
			vector.setFromMatrixPosition( object.matrixWorld );
			vector.applyMatrix4( viewProjectionMatrix );

			var element = object.element;
			var style = 'translate(-50%,-50%) translate(' + ( vector.x * _widthHalf + _widthHalf ) + 'px,' + ( - vector.y * _heightHalf + _heightHalf ) + 'px)';

			element.style.WebkitTransform = style;
			element.style.MozTransform = style;
			element.style.oTransform = style;
			element.style.transform = style;

			var objectData = {
				distanceToCameraSquared: getDistanceToSquared( camera, object )
			};

			cache.objects.set( object, objectData );
			if ( element.parentNode !== domElement ) {
				domElement.appendChild( element );
			}
		}

		for ( var i = 0, l = object.children.length; i < l; i ++ ) {
			renderObject( object.children[ i ], camera );
		}
	};
	var getDistanceToSquared = function () {
		var a = new THREE.Vector3();
		var b = new THREE.Vector3();
		return function ( object1, object2 ) {
			a.setFromMatrixPosition( object1.matrixWorld );
			b.setFromMatrixPosition( object2.matrixWorld );
			return a.distanceToSquared( b );
		};
	}();

	var filterAndFlatten = function ( scene ) {
		var result = [];
		scene.traverse( function ( object ) {
			if ( object instanceof CSS2DObject ) result.push( object );
		} );
		return result;
	};
	var zOrder = function ( scene ) {
		var sorted = filterAndFlatten( scene ).sort( function ( a, b ) {
			var distanceA = cache.objects.get( a ).distanceToCameraSquared;
			var distanceB = cache.objects.get( b ).distanceToCameraSquared;
			return distanceA - distanceB;
		} );

		var zMax = sorted.length;
		for ( var i = 0, l = sorted.length; i < l; i ++ ) {
			sorted[ i ].element.style.zIndex = zMax - i;
		}

	};

	this.render = function ( scene, camera ) {
		scene.updateMatrixWorld();
		if ( camera.parent === null ) camera.updateMatrixWorld();
		viewMatrix.copy( camera.matrixWorldInverse );
		viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, viewMatrix );
		renderObject( scene, camera );
		zOrder( scene );
	};
};
export {CSS2DRenderer,CSS2DObject}

CSS2DRenderer 是 Three.js 中的一个渲染器,用于将 HTML 元素嵌入到 Three.js 场景中并进行渲染。它的源码主要由以下几个部分组成:

  1. 构造函数

CSS2DRenderer 的构造函数非常简单,只需要创建一个 CSS2DRenderer 对象,并将其 domElement 属性设置为一个 div 元素。

THREE.CSS2DRenderer = function () {
  // 创建一个 div 元素
  var domElement = document.createElement('div');
  domElement.style.position = 'absolute';
  domElement.style.top = '0';
  domElement.style.pointerEvents = 'none';

  // 将 div 元素作为 CSS2DRenderer 对象的 domElement 属性
  this.domElement = domElement;
};
  1. setSize 方法

CSS2DRenderer 的 setSize 方法用于设置渲染器的尺寸。该方法将设置 domElement 元素的宽度和高度属性,并将其样式设置为 inline-block。

THREE.CSS2DRenderer.prototype.setSize = function (width, height) {
  this.domElement.style.width = width + 'px';
  this.domElement.style.height = height + 'px';
  this.domElement.style.display = 'inline-block';
};
  1. render 方法

CSS2DRenderer 的 render 方法用于渲染场景中的 HTML 元素。该方法会遍历场景中的所有 CSS2DObject 对象,并将其 element 属性添加到 domElement 元素中。

THREE.CSS2DRenderer.prototype.render = function (scene, camera) {
  // 遍历场景中的所有 CSS2DObject 对象
  var objects = scene.__cssObjects;
  for (var i = 0, l = objects.length; i < l; i++) {
    var object = objects[i];
    var element = object.element;
    if (element.parentNode !== this.domElement) {
      this.domElement.appendChild(element);
    }
  }
};
  1. dispose 方法

CSS2DRenderer 的 dispose 方法用于释放渲染器占用的资源。该方法会将 domElement 元素从文档中移除,并将其设置为 null。

THREE.CSS2DRenderer.prototype.dispose = function () {
  this.domElement.parentNode.removeChild(this.domElement);
  this.domElement = null;
};
  1. CSS2DObject 类

CSS2DObject 类用于封装 HTML 元素,并在 Three.js 场景中使用。该类的构造函数接受一个 HTMLElement 参数,用于指定封装的 HTML 元素。

THREE.CSS2DObject = function (element) {
  // 创建一个 div 元素,并将传入的 HTMLElement 添加到其中
  var domElement = document.createElement('div');
  domElement.appendChild(element);
  domElement.style.position = 'absolute';
  domElement.style.pointerEvents = 'none';

  // 将创建的 div 元素作为 CSS2DObject 对象的 element 属性
  this.element = domElement;
};
  1. updateMatrixWorld 方法

CSS2DObject 的 updateMatrixWorld 方法用于更新对象的变换矩阵。该方法会调用 CSS2DObject 的父类 Object3D 的 updateMatrixWorld 方法,并将 element 元素的位置和旋转与 CSS2DObject 对象的变换矩阵同步。

THREE.CSS2DObject.prototype.updateMatrixWorld = function (force) {
  // 调用父类 Object3D 的 updateMatrixWorld 方法
  THREE.Object3D.prototype.updateMatrixWorld.call(this, force);

  // 同步 element 元素的位置和旋转
  var style = this.element.style;
  style.transform = 'translate(-50%,-50%) translate(' + (this.position.x * window.innerWidth / 2) + 'px,' + (-this.position.y * window.innerHeight / 2) + 'px)';
  style.zIndex = Math.round((1 - this.position.z) * 10000);
};

通过上述源码解读,我们可以了解到 CSS2DRenderer 的实现原理和主要功能。它通过将 HTML 元素封装成 CSS2DObject 对象,并在 render 方法中将 element 元素添加到场景中,从而实现了将 HTML 元素嵌入到 Three.js 场景中的效果。同时,CSS2DObject 的 updateMatrixWorld 方法还可以将element 元素的位置和旋转与 CSS2DObject 对象的变换矩阵同步,从而实现更加精细的控制效果。


文章首发:webgl.blog.csdn.net/article/det…