最近需要给自个儿项目的模型加上标签,来显示一些有的没的的数据。
去例子萌里瞄了一眼,发现了css2dlabel 这个例子。
远瞅是这样的!其实近瞅也这样,这个顶着一个moon的月亮会绕着这个地球一直旋转,上面的标签也会和他一起移动。(我总感觉应该顶一个奋斗当标签)
闲言少叙,来看看咋用的。
调用篇
拿月球举例子吧
生成球。这个瞄一眼应该不会有太大的问题。
shininess。默认是30,看起来会有明显的高光点。而月球表面那么多石灰一样的东西,太亮不太真,所以调暗点。
下面是30和5的对比,大小应该一样,截屏没控制好。
生成标签如下:
正常生成一个文字为Moon的div,label类里有点样式,不多。要想丰富样式就多改改这里。
重点来了,他生成了一个CSS2DObject类,传入了那个div, 并把它举到了月亮头顶。加到了月亮坐标系。
如果新建了一个三维的无论什么东西,加到月亮里面都是会跟着月亮一起动的。
可是,我们加的是二维的。不在一个维度里。这也是这个例子最有趣的地方。
Init函数里示例化了一个CSS2DRenderer,这个类里面有一个超大的隐藏的div来管理所有的label,这个下面读源码的时候还会分析,先瞄一眼过,应该问题不大。
调用部分剩下就是,让月亮转转,刷新一下labelRenderer,最后这个才是这盘菜所有的精华。
核心篇
这里总管两个类,一个CSS2DObject,一个CSS2DRenderer
CSS2DObject 这个类没有太大的用处,基本是用来类型识别,本身也很简单
用来类型识别
上面的这个监听很有趣,不知道干嘛的,印象里也没有这个监听,难道要自定义使用?类似于下面这样?参考代码来自:这个小哥
window.addEventListener("testEvent", function(obj){
console.log(obj.data);
alert("触发成功!")
});
function clickHere() {
// 创建自定义事件
var event = document.createEvent("HTMLEvents");
// 初始化testEvent事件
event.initEvent("testEvent", false, true);
event.data = {"click":true};
// 触发自定义事件
window.dispatchEvent(event);
}
查了一下Dom事件也没发现。
CSS2DRenderer类里面负责了主要的操作和更新。
先看一下这个类里面的定义的变量
前面说过,这个里面会定义一个hidden的大的div,大概就是屏幕的分辨率,来管理所有的label,这里的长宽高都是这个div相关的属性。
vector记录投影变换后,在投影坐标系里面的坐标值。
研究清楚了这个我突然知道CSS2DObject这个类是干啥用的了,里面没有几行代码,我以为只是用来类型区分的,其实不是,我上面说,如果是一个三维物体扔在一个移动的父物体上,这个三维物体是会跟随父物体运动的,所以这货的意义就在于,它把一个label纯二维的东西,其实给包装成了一个三维的物体,让这个三维物体跟着父物体运动,反算这个三维物体的屏幕坐标,也就知道了label的屏幕偏移量。感觉好有趣,果然每个人实现的脑回路都是不一样的。如果是这样计算的话,其实直接计算实体的屏幕坐标也是可以的,比如那个月亮,在给一点二维上的偏移。
继续说这个vector,用到的就3行,还是蛮考验基础的。
如果一个物体是CSS2DObject的话,就从它的世界矩阵中获取这哥们的世界坐标。
顺便也可以把定义的viewProjectionMatrix变量也解释了。
我的一个 CSS2DObject 获得了世界坐标后,又转换到了相机的坐标系,然后进行了投影变换,可以简单理解为投影到了屏幕。这就是第二行做的事,最后一行,希望的尽量解释清楚。
translate(-50%,-50%) 这个变换如果不加,label偏右偏下,为了让它居中,加了这个。如下:
后面的translate是为了将投影变换后的坐标,和屏幕的坐标相对应。如下:
我们有的是第一个坐标系的坐标,要获得第二个坐标系的,这个映射自己回去导,就是后半段的translate。
八卦一下viewProjectionMatrix矩阵是怎么获得的,
首先把scene里面的所有的物体撸一遍更一下世界坐标,然后更一下相机的世界坐标,
viewMatrix保存的是从世界坐标系转换到相机坐标系需要的矩阵。
在乘上projectionMatrix,就可以换到投影坐标系了~
(zOrder是自带的函数,rayObj是我自己根据需要加上的)
cache里的 WeakMap() 据说是ES6的新玩具,查了一下,瞄了一眼别人家的文章,主要的特点就以下几个:
key必须是object,且不可枚举,也就是说不可以被遍历。
没有size。
它的出现对垃圾回收友好,就是说没有引用的话可以袅袅悄悄的被回收。因为是“弱指针”。(最近满世界都对垃圾很关注嘛)
这个工程里是这样用滴。
还发现了一个好用的函数 scene.traverse 这家伙可以遍历 scene里面所有的物体。
周五没写完,隔了两天在写这篇像喝断片了一样。= =,看来还是要一鼓作气啊~
查了一下源码,发现这是Object3D.prototype里面的函数,大部分的类都是继承自这个类的,也就是说,大部分的实例都有这个函数。源码也不复杂,封装完的代码还是很清爽的。
因为项目的需要,在原有功能的基础上增加了遮挡隐藏和距离限制显示的功能。
简单来说就是如果地球挡住了月亮,就不显示月亮的标签。
如果在控制相机的过程中,和月球/地球的距离过近或过远都会隐藏标签。
有时候项目的标签不止一两个,甚至可能会很多,这样可以有效突出正在关注的标签。
增加了一个函数rayObj,接受四个参数(scene,camera, near,far)
最后两个如果不传参,相当于没有限制。
获得相机和含有CSS2DObject标签的物体的位置
判断是否在距离以内
判断是否有遮挡
从相机朝着物体发了一个射线,看看有没有遮挡。
因为有人私信问过我,所以我把疑似当年的CSS2DRenderer.js又翻出来了。给大家一个完整的参考。
/**
* CSS2DRenderer.js
*/
THREE.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 );
}
} );
};
THREE.CSS2DObject.prototype = Object.create( THREE.Object3D.prototype );
THREE.CSS2DObject.prototype.constructor = THREE.CSS2DObject;
//
THREE.CSS2DRenderer = function () {
console.log( 'THREE.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 THREE.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 = [];
( function ( object ) {
if ( object instanceof THREE.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.visibility = "visible";
sorted[ i ].element.style.zIndex = zMax - i;
}
};
var rayObj = function(scene,camera, near,far)
{
this.near = near || 0;
this.far = far || Infinity;
scene.traverse( function ( object ) {
if ( object instanceof THREE.CSS2DObject )
{
let visible = "visible";
let posCam = new THREE.Vector3().setFromMatrixPosition( camera.matrixWorld );
let posMesh = new THREE.Vector3().setFromMatrixPosition( object.parent.matrixWorld );
// 判断是否在距离内
let dis = posCam.clone().distanceTo(posMesh);
if(visible == "visible")
{
if(dis < this.near || dis > this.far)
{
visible = "hidden";
}
}
// 判断是否有遮挡
if(visible == "visible")
{
let dir = posMesh.clone().sub(posCam).normalize();
let raycaster = new THREE.Raycaster(posCam, dir,camera.near,camera.far);
let intersects = raycaster.intersectObjects( scene.children );
for(let i = 0; i < intersects.length; i++)
{
if(intersects[i].object instanceof THREE.AxesHelper)
{
continue;
}
if(intersects[i].object == object.parent)
{
continue;
}
visible = "hidden";
}
}
object.element.style.visibility = visible;
}
} );
};
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 );
rayObj(scene,camera);
};
};
css2d_label.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>three.js css2d - label</title>
<style>
body {
background-color: #000;
margin: 0;
overflow: hidden;
}
#info {
position: absolute;
top: 0px;
width: 100%;
color: #FFF;
padding: 5px;
font-family: Monospace;
font-size: 13px;
text-align: center;
z-index: 1;
}
.label{
width: 80px;
height: 30px;
text-align: center;
color: #FFF;
font-family: sans-serif;
padding: 2px;
background: url("textures/labelBg.png");
background-size: 80px 32px;
}
a {
color: #000000;
}
</style>
</head>
<body>
<div id="info"><a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - three.js css2d - label</div>
<div id="tets"></div>
<script src="threejs/three.js"></script>
<script src="threejs/OrbitControls.js"></script>
<script src="threejs/CSS2DRenderer.js"></script>
<script>
var camera, scene, renderer, labelRenderer;
var clock = new THREE.Clock();
var textureLoader = new THREE.TextureLoader();
var earth, moon;
init();
animate();
function init() {
var EARTH_RADIUS = 1;
var MOON_RADIUS = 0.27;
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.set( 10, 5, 20 );
var controls = new THREE.OrbitControls( camera );
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x333333 );
var dirLight = new THREE.DirectionalLight( 0xffffff );
dirLight.position.set( 0, 0, 1 );
scene.add( dirLight );
var axesHelper = new THREE.AxesHelper( 5 );
scene.add( axesHelper );
//
var earthGeometry = new THREE.SphereBufferGeometry( EARTH_RADIUS, 32, 32);
var earthMaterial = new THREE.MeshPhongMaterial( {
specular: 0x333333,
shininess: 5,
map: textureLoader.load( 'textures/planets/earth_atmos_2048.jpg' ),
specularMap: textureLoader.load( 'textures/planets/earth_specular_2048.jpg' ),
normalMap: textureLoader.load( 'textures/planets/earth_normal_2048.jpg' ),
normalScale: new THREE.Vector2( 0.85, 0.85 )
} );
earth = new THREE.Mesh( earthGeometry, earthMaterial );
earth.name = "earth";
scene.add( earth );
var moonGeometry = new THREE.SphereBufferGeometry( MOON_RADIUS, 16, 16 );
var moonMaterial = new THREE.MeshPhongMaterial( {
shininess: 5,
map: textureLoader.load( 'textures/planets/moon_1024.jpg' )
} );
moon = new THREE.Mesh( moonGeometry, moonMaterial );
moon.position.set(10,0,0);
moon.name = "moon";
scene.add( moon );
//
var earthDiv = document.createElement( 'div' );
earthDiv.className = 'label';
earthDiv.textContent = 'Earth';
earthDiv.style.marginTop = '-1em';
var earthLabel = new THREE.CSS2DObject( earthDiv );
earthLabel.position.set( 0, EARTH_RADIUS, 0 );
earth.add( earthLabel );
var moonDiv = document.createElement( 'div' );
moonDiv.className = 'label';
moonDiv.textContent = 'Moon';
moonDiv.style.marginTop = '-1em';
var moonLabel = new THREE.CSS2DObject( moonDiv );
moonLabel.position.set( 0, MOON_RADIUS, 0 );
moon.add( moonLabel );
//
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
labelRenderer = new THREE.CSS2DRenderer();
labelRenderer.setSize( window.innerWidth, window.innerHeight );
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = 0;
document.body.appendChild( labelRenderer.domElement );
}
function animate() {
requestAnimationFrame( animate );
var elapsed = clock.getElapsedTime();
moon.position.set( Math.sin( elapsed ) * 5, 0, Math.cos( elapsed ) * 5 );
renderer.render( scene, camera );
labelRenderer.render( scene, camera );
}
</script>
</body>
</html>