3d模型动态热力图

更新时间: 2025-03-06 13:18:18

前天公司想让我做在模型上实时变化的动态热力图的效果,我已经写完了,这篇文章简单复盘一下,先看效果吧:

# 原理

我找到三个位置作为传感器安装的位置,并模拟了传感器的数据

一开始我使用的方式是给模型的每个顶点计算插值,根据这个插值来计算出顶点颜色,然后使用顶点着色来实现热力图。
但很快我发现了弊端,这个计算过程太慢了,尤其是模型顶点一多计算量就相当庞大,但要是减少模型顶点数量热力图的效果又会非常差

后来我换了一个路线,使用了shader,将三个位置和数据作为uniform传递,然后将顶点位置传递给片元着色器,根据每个位置来计算出插值和颜色即可
这样的计算使用的是GPU计算,效率大大提高,而且效果不受模型顶点数量的限制

# 导入模型并替换材质

导入模型后,将模型所有材质都替换成shaderMaterial

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( '/draco/gltf/' );
dracoLoader.setDecoderConfig( { type: 'js' } );
dracoLoader.preload()  

let loader = new GLTFLoader();/*实例化加载器*/
loader.crossOrigin = 'anonymous';
loader.setCrossOrigin('anonymous');
loader.setDRACOLoader(dracoLoader)
loader.setMeshoptDecoder(MeshoptDecoder);  

planeMaterial = new THREE.ShaderMaterial({
        alphaTest: 0.1,
        side: THREE.DoubleSide,
        transparent: true
    })

    loader.load("/model/hottest.glb", (gltf) => {
        let object = gltf.scene
        scene.add(object)
        object.traverse(item => {
            if (item.type == 'Mesh') {
                item.material = planeMaterial
            }
        })
    }, undefined, function (error) {
        console.log(error)
    });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这样就替换好了,现在模型整个都会变成红色就成功了。

# 核心代码

# 顶点着色器

vertexShader没什么好说的,直接上代码,这段代码就是不给模型的顶点位置做任何变化

const planeVertexShader = `
    varying vec2 vUv;
    varying vec3 vPosition;

    void main() {
        vUv = uv;
        vPosition = position;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`
1
2
3
4
5
6
7
8
9
10
11

# 模拟数据

首先创建三个模拟的位置和对应的数据:

let dataList = [
    {
        id:1,
        point:{
            "x": -225.30913213043527,
            "y": 29.46935203240338,
            "z": 4.053905888764007
        },
        data: 200
    },
    {
        id:2,
        point:{
            "x": 236.7434205863005,
            "y": 29.38562844352316,
            "z": 0.2052973440746655
        },
        data: 70
    },
    {
        id:3,
        point:{
            x:-0.8774502890408371,
            y:31.64863246132103,
            z:2.0555340513851093
        },
        data: 80
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

将数据改造成vec4的形式传给uniform,前三个分量是坐标xyz,第4个分量是data

   const dataUniform = dataList.map(item => {
        const ret = new THREE.Vector4(item.point.x, item.point.y, item.point.z, item.data)
        return ret
    })

    planeMaterial = new THREE.ShaderMaterial({
        alphaTest: 0.1,
        side: THREE.DoubleSide,
        transparent: true,
        vertexShader : planeVertexShader,
        uniforms:{
            uDataList: {value: dataUniform}
        }
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 片元着色器

片元着色器用接收到的vPosition顶点位置,和uniform中的位置以及数据,计算出每个片元应该显示的颜色

const planeFragmentShader = `
        varying vec2 vUv; 
        varying vec3 vPosition;
        uniform vec4 uDataList[3];

         float distanceToSquared( vec4 c, vec3 v ) {

            float dx = c.x - v.x;
            float dy = c.y - v.y;
            float dz = c.z - v.z;

            return dx * dx + dy * dy + dz * dz;

        }


        float distanceTo(vec4 c, vec3 v ) {

            return sqrt( distanceToSquared(c, v ) );

        }

        vec3 valueColor(float move) {
            vec3 colorList[] = vec3[](vec3(0.0,0.0,1.0),vec3(0.0,1.0,1.0), vec3(1.0,1.0,0.0),vec3(1.0,0.0,0.0));
            if(move >= 0.0 && move < 0.4) {
                return mix(colorList[0], colorList[1], move / 0.4);
            }else if(move >= 0.4 && move < 0.7) {
                return mix(colorList[1], colorList[2], (move - 0.4) / 0.3);
            }else if(move >= 0.7 && move < 1.0) {
                return mix(colorList[2], colorList[3], (move - 0.7) / 0.3);
            }else if(move >= 1.0){
                return colorList[3];
            }else{
                return colorList[0];
            }
        }


        void main() {
            float influence = 100.0;
            vec4 comparePoint;
            float min = 0.0;
            float max = 100.0;
            float dis;

            for (int i = 0; i < 3; i++) {
                float len = distanceTo(uDataList[i],vPosition);
                if (i == 0) {
                    dis = len;
                    comparePoint = uDataList[i];
                } else {
                    if (len < dis) {
                        dis = len;
                        comparePoint = uDataList[i];
                    }
                }
            }

            float disVal;
            if (dis < influence) {
                disVal = comparePoint.w * ((influence - dis) / influence);
            }
            float move = disVal / (max - min);    
       
            vec3 color = valueColor(move);
            gl_FragColor = vec4(color, 1.0);
        }
    `
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

然后不断的更新uniform中的数据就可以让热力图动起来了