总要有人来改变世界的,为什么不能是你呢

three.js效果之热力图和轨迹线

1.热力图

开始的时候,是用一个网上找的canvas画渐变热点的demo,原理就是给定顶点坐标,然后画圆,颜色使用渐变色,根据权重决定渐变的层数(红色->橙色->绿色) 。

但是终究觉得这种方法不仅繁琐,而且画出来的效果不够自然。

后来发现有一个开源库heatmap效果很好,它是这样用的(官方demo地址链接):

var heatmapInstance = h337.create({
    container: document.querySelector('.heatmap')
});
var data = {
    max: max,
    data: points
};
heatmapInstance.setData(data);

max值为所有points中权重属性的最大值。

看到这里,那我们要怎么在three.js中去使用heatmap呢,他是用dom去实例化heatmap对象的啊。

不用担心,我们可以creatElement('div'),然后在这个dom对象上实例化heatmap对象,并且

      var canvas = heatmapdiv.getElementsByTagName('canvas')[0];

获取绘制后的canvas对象。

        let heatMapGeo = new THREE.PlaneGeometry(120,90);
        let heatMapTexture = new THREE.Texture(canvas);
        let heatMapMaterial = new THREE.MeshBasicMaterial({
            map: heatMapTexture,
            transparent:true
        });
        heatMapMaterial.map.needsUpdate = true;
        var heatMapPlane = new THREE.Mesh(heatMapGeo,heatMapMaterial);
        heatMapPlane.position.set(0,0.1,0);
        heatMapPlane.rotation.x = -Math.PI/2;
        this.scene.add(heatMapPlane);

这样,用heatmap绘制的热力图就添加到了three.js创建的场景中去了。

2.轨迹线

轨迹线不难想,利用three.js提供的曲线来绘制,但是会存在如下两个问题:

q1.three.js的曲线貌似只能一次性整条绘制出来,没有api显示可以按百分比绘制曲线,所以只好自己写shader实现

q2.webgl渲染器不支持线宽属性(three.MeshLine支持线宽,不过没有研究是否支持按百分比绘制);

q3.着色器里面可以针对点设置pointsize来实现点的大小(间接实现曲线的宽度控制),但是点是二维的,默认存在于x-y平面,所以在x-z平面看的时候,如果点的数量不够多那么就会出现断断续续的效果,但是采样的点数量足够多又会影响性能。

上述的问题不能解决的话,后续的曲线样式优化(渐变)就无从谈起。

期间我想过,既然点存在于x-y平面,那么我们就将x-z平面的轨迹放到x-y平面来绘制,最后将这条线绕x轴旋转90度,但是因为对点进行处理的时候,首先正方形的点->圆点->渐变(抗锯齿),最后,结果如下:

看着好像成功了,但是由于深度检测机制(现在想来,是不是可以设置取消这条线的深度检测机制)的存在,某些角度下,这条线的本质(n个大号的点拼接)就变得很明显了,你会明显地看到这条线是由进行抗锯齿处理后的无数个点组成。

哎,好像又遇到困难了啊。

后来一想,既然three.js中一条线很细,那么10条线,100条线在一起呢?只要间距足够小,它们看上去就是一根线,一根麻绳!!!

PS:

回过头来看我当时的这个处理思路,其实在性能上还是有很大的提升空间的,至少增加了内存消耗;

当然后续查看meshline源码,你会发现他是将顶点作偏移绘制三角面来实现有宽度的“线”,并且每个顶点还传入了顶点的索引值属性,可用于进度的控制;

照着这个思路, 写了一个FatLine类:

import * as THREE from 'three'
/**
 * Author:桔子桑
 * Time:2019.10.12
 */

const vs =`
    varying vec3 iPosition;

    void main(){
        iPosition = vec3(position);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x,0.2,position.z,1.0);
    }
`;
const fs = `
    uniform float time;
    varying vec3 iPosition;
    uniform float alpha;

    void main( void ) {
        if(iPosition.y > time){
            discard;
        }else{
            gl_FragColor = vec4(0.813,0.124,0.334,alpha); 
        }

    }
`;

function FatLine(vertices,width,scene){
    this.width = width;
    this.vertices = vertices;
    this.start = 0;
    this.scene = scene;
    this.linearr = [];
    this.lines = [];
}
function createMaterial(vs, fs, start) {
    var attributes = {};
    var uniforms = {
        time: {type: 'f', value: start},
        size:{type:'f',value:25.0},
    };
    var meshMaterial = new THREE.ShaderMaterial({
        uniforms: uniforms,
        defaultAttributeValues : attributes,
        vertexShader: vs,
        fragmentShader: fs,
        transparent: true
    });
    return meshMaterial;
}

FatLine.prototype.draw = function() {
    var size = this.vertices.length;
    var length = Math.floor(this.width/2);
    var vm = this;
    for(var j =0;j<length;j++){
        var lineadd = [];
        var linereduce = [];
        for ( var i = 0; i <size; i ++ ) {
            var Vector3 = this.vertices[i],
            x = Vector3.x,
            y = Vector3.y,
            z = Vector3.z;
            var zadd = z+j*0.001;
            var zreduce = z-j*0.001;
            lineadd.push( new THREE.Vector3(x,y,zadd ));
            linereduce.push( new THREE.Vector3(x,y,zreduce ));
        }
        this.linearr.push(lineadd);
        this.linearr.push(linereduce);
    };
    this.linearr.push(vm.vertices);
    var pointsize = this.vertices.length * 10;
    for(var k = 0,size=this.linearr.length;k<size;k++){
        var vertices = this.linearr[k];
        var alpha = (Math.floor(size/2) - Math.floor(k/2))/Math.floor(size/2);
        var curve = new THREE.CatmullRomCurve3(vertices);
        var geometry = new THREE.Geometry();
        geometry.vertices = curve.getPoints(pointsize);
        var material = createMaterial(vs,fs,vm.start);
        material.uniforms.alpha = {type:'f',value:alpha};
        var line = new THREE.Line(geometry, material);
        this.lines.push(line);
        this.scene.add( line );
    }
}

FatLine.prototype.animate = function(speed,callback){
    var time = this.lines[0].material.uniforms.time.value;
    for(var i = 0,length=this.lines.length;i<length;i++){
        var line = this.lines[i];
        line.material.uniforms.time.value +=speed||0.3;
    };
    if(callback){
        callback(time);
    }
}

export default FatLine;

你可以看到,着色器中还又一个uniform变量time,这个是用来在FatLine开启动画的时候,随着时间的进展来逐步绘制的。

ok,看到这你以为就完了?no!!!

刚开始的时候,按照常规当time++的时候,在x-z平面上轨迹点,我们判断x<time是否来控制曲线的绘制进度,但是一个问题出现了,人员轨迹点可能出现在一个房间兜圈子的情况(实际也是如此),这样就会存在第2个点和第200个点都满足x<2.0,那么总不能根据时间,第2秒的时候,直接把200秒时候的点也绘制出来了吧,这是不符合常理的。

在下班回家的路上,我想到了一个问题,在三维空间,一个点有(x,y,z)三个维度的坐标信息数据传进了着色器里面,但是我们的人员轨迹只会存在于场景的x-z平面,所以这个y坐标值在着色器里面是没有用到的,哈哈,那么这个y值可以充当时间维度值,第一个点y=1,第二个点y=2,第三个点y=3...,如此一来,当time++的时候,我们只要判断y<time就可以实现在时间维度上的控制了。

并且FatLine的animate函数还提供了一个回调函数,参数值是当前的time值,所以你可以用这个time值来绘制具体的点:

     if(this.FatLine){
          function addpoint(time){
            for(var i = 0,length=vm.vertices.length;i<length;i++){
              var point = vm.vertices[i];
              if(Math.abs(point.y-time)< 0.1){
                vm.addpoint(point.x,point.z);
              }
            }
          }
          this.FatLine.animate(0.2,addpoint);
        }

每一帧都会animate一下,也就是time++,并且判断进度是不是到了指定的某个点上,如果到了,那么就顺便把这个点也画出来,就像上述的动图一样。

最终可控制粗细和运动速度的曲线就完成了。

 

posted @ 2019-10-13 19:44  桔子桑  阅读(10902)  评论(8编辑  收藏  举报