【可视化学习】31-智慧城市(三)
发表于:2023-07-08 |

前言

从本篇开始,我们就开始进入智慧城市效果添加的环节了

添加模型渐变效果

首先,因为我之前的天一广场的模型海拔区分不是很大,展示效果就不是很好,所以我就换了个模型使用

了解更换的模型

模型效果如下
模型效果

了解材质内部代码

我们新建一个文件modifyCityMaterial.js,在这里面写我们的修改城市材质的代码
在createMesh.js中将物体传进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import * as THREE from "three";
import scene from "../scene";
import modifyCityMaterial from "../modify/modifyCityMaterial";

export default function createCity() {
const gltfLoader = new GLTFLoader();
gltfLoader.load("./model/city.glb", (gltf) => {
gltf.scene.traverse((item) => {
if (item.type == "Mesh") {
const cityMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(0x0c0e33),
});
item.material = cityMaterial;
modifyCityMaterial(item);
}
});
scene.add(gltf.scene);
});
}

我们知道,这个物体模型已经被我们修改成了标准材质,因此很多其他材质有的效果我们都用不了,最好的就是我们自己修改shader,此时就用到了我们之前学过的一个用法,onBeforeCompile,我们在这里面修改shader

1
2
3
4
5
6
7
import * as THREE from "three";
export default function modifyCityMaterial(mesh) {
mesh.material.onBeforeCompile = (shader) => {
console.log(shader.vertexShader);
console.log(shader.fragmentShader);
};
}

了解物体内部基础材质

实现渐变效果

既然想实现这样的效果,我们至少得知道这个模型的最大值和最小值,然后通过混合百分比的方法来实现
那如何来得到最大值和最小值呢,这个其实很简单,我们之前介绍小黄鸭外立方体的时候说过,可以通过boundingBox来获取,实现代码如下
,其他地方都好理解,我着重讲一下这一行代码的问题

1
float gradMix = (vPosition.y+uHeight/2.0)/uHeight;

这是因为我们uHeight是最大值减去最小值,而因为最小值是负值,比如这个最大值是86,最小值是-86,要将这个转换成0%-100%,那么就可以理解这段代码了

还有这个

1
//#end#

这个是我们用来追加着色器效果使用的,每一次用end来标记,这样我们就可以在这个标记后面追加我们的着色器代码了

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
69
70
export default function modifyCityMaterial(mesh) {
mesh.material.onBeforeCompile = (shader) => {
shader.fragmentShader = shader.fragmentShader.replace(
"#include <dithering_fragment>",
`
#include <dithering_fragment>
//#end#
`
);
addGradColor(shader, mesh);
};
}
export function addGradColor(shader, mesh) {
mesh.geometry.computeBoundingBox();
// console.log(mesh.geometry.boundingBox);

let { min, max } = mesh.geometry.boundingBox;
// 获取物体的高度差
let uHeight = max.y - min.y;

shader.uniforms.uTopColor = {
value: new THREE.Color("#aaaeff"),
};
shader.uniforms.uHeight = {
value: uHeight,
};

shader.vertexShader = shader.vertexShader.replace(
"#include <common>",
`
#include <common>
varying vec3 vPosition;
`
);

shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
`
#include <begin_vertex>
vPosition = position;
`
);

shader.fragmentShader = shader.fragmentShader.replace(
"#include <common>",
`
#include <common>

uniform vec3 uTopColor;
uniform float uHeight;
varying vec3 vPosition;

`
);
shader.fragmentShader = shader.fragmentShader.replace(
"//#end#",
`

vec4 distGradColor = gl_FragColor;

// 设置混合的百分比
float gradMix = (vPosition.y+uHeight/2.0)/uHeight;
// 计算出混合颜色
vec3 gradMixColor = mix(distGradColor.xyz,uTopColor,gradMix);
gl_FragColor = vec4(gradMixColor,1);
//#end#

`
);
}

打印出来的最大最小值
渐变效果

城市光圈扩散效果

我们设置初始的中心点uSpreadCenter,以及time:uSpreadTime,宽度uSpreadWidth
以下代码我们着重讲解一下这三行代码

1
2
3
4
5
6
7
 float spreadRadius = distance(vPosition.xz,uSpreadCenter);
// 扩散范围的函数
float spreadIndex = -(spreadRadius-uSpreadTime)*(spreadRadius-uSpreadTime)+uSpreadWidth;

if(spreadIndex>0.0){
gl_FragColor = mix(gl_FragColor,vec4(1,1,1,1),spreadIndex/uSpreadWidth);
}

我们的扩散光圈打到建筑物上面,是需要和建筑物原有的颜色进行叠加的,那么我们可以知道,越扩散的时候,光圈就会越来,我们打到建筑物的位置也会不同,而混合需要一个0%-100%的值,那么首先得保证扩散函数的值可以大于0,那么我们就想到了负的一个一元二次方程,如果再加一个值,那么就是一个抛物线,且有值大于y===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
// 添加建筑材质光波扩散特效
export function addSpread(shader, center = new THREE.Vector2(0, 0)) {
// 设置扩散的中心点
shader.uniforms.uSpreadCenter = { value: center };
// 扩散的时间
shader.uniforms.uSpreadTime = { value: -2000 };
// 设置条带的宽度
shader.uniforms.uSpreadWidth = { value: 40 };

shader.fragmentShader = shader.fragmentShader.replace(
"#include <common>",
`
#include <common>

uniform vec2 uSpreadCenter;
uniform float uSpreadTime;
uniform float uSpreadWidth;
`
);

shader.fragmentShader = shader.fragmentShader.replace(
"//#end#",
`
float spreadRadius = distance(vPosition.xz,uSpreadCenter);
// 扩散范围的函数
float spreadIndex = -(spreadRadius-uSpreadTime)*(spreadRadius-uSpreadTime)+uSpreadWidth;

if(spreadIndex>0.0){
gl_FragColor = mix(gl_FragColor,vec4(1,1,1,1),spreadIndex/uSpreadWidth);
}

//#end#
`
);

gsap.to(shader.uniforms.uSpreadTime, {
value: 800,
duration: 3,
ease: "none",
repeat: -1,
});
}

直线光带掠过城市效果

上面的如果看懂了,那么下面的就很简单了,我们只需要设置一个直线的方程,然后计算出直线的距离,然后计算出混合的百分比就可以了,代码如下

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
export function addLightLine(shader) {
// 扩散的时间
shader.uniforms.uLightLineTime = { value: -1500 };
// 设置条带的宽度
shader.uniforms.uLightLineWidth = { value: 200 };

shader.fragmentShader = shader.fragmentShader.replace(
"#include <common>",
`
#include <common>


uniform float uLightLineTime;
uniform float uLightLineWidth;
`
);

shader.fragmentShader = shader.fragmentShader.replace(
"//#end#",
`
float LightLineMix = -(vPosition.x+vPosition.z-uLightLineTime)*(vPosition.x+vPosition.z-uLightLineTime)+uLightLineWidth;

if(LightLineMix>0.0){
gl_FragColor = mix(gl_FragColor,vec4(0.8,1.0,1.0,1),LightLineMix /uLightLineWidth);

}

//#end#
`
);

gsap.to(shader.uniforms.uLightLineTime, {
value: 1500,
duration: 5,
ease: "none",
repeat: -1,
});
}

从下到上扫描城市特效

以此类推,这个只需要将x,z改成y就可以了,代码如下

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
export function addToTopLine(shader) {
// 扩散的时间
shader.uniforms.uToTopTime = { value: 0 };
// 设置条带的宽度
shader.uniforms.uToTopWidth = { value: 40 };

shader.fragmentShader = shader.fragmentShader.replace(
"#include <common>",
`
#include <common>


uniform float uToTopTime;
uniform float uToTopWidth;
`
);

shader.fragmentShader = shader.fragmentShader.replace(
"//#end#",
`
float ToTopMix = -(vPosition.y-uToTopTime)*(vPosition.y-uToTopTime)+uToTopWidth;

if(ToTopMix>0.0){
gl_FragColor = mix(gl_FragColor,vec4(0.8,0.8,1,1),ToTopMix /uToTopWidth);

}

//#end#
`
);

gsap.to(shader.uniforms.uToTopTime, {
value: 500,
duration: 3,
ease: "none",
repeat: -1,
});
}

实现飞线效果

首先我们按照如下结构生成一个flyLine的文件
结构图
我们可以看到,官网的这个管道非常适合我们的需求
官网管道文档图

利用官网的管道生成飞线

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
import * as THREE from "three";
import gsap from "gsap";

export default class FlyLine {
constructor() {
let linePoints = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(5, 4, 0),
new THREE.Vector3(10, 0, 0),
];
// 1/创建曲线
this.lineCurve = new THREE.CatmullRomCurve3(linePoints);
// 2/根据曲线生成管道几何体
this.geometry = new THREE.TubeGeometry(
this.lineCurve,
100,
0.4,
2,
false
);

// 3/设置飞线材质
// 创建纹理
this.material = new THREE.MeshBasicMaterial({
color: 0xfff000,
transparent: true,
});



// 4/创建飞线物体
this.mesh = new THREE.Mesh(this.geometry, this.material);
}
}

飞线图

给飞线添加贴图

我们使用这一张贴图来修改材质
贴图

1
2
3
4
5
6
const textloader = new THREE.TextureLoader();
this.texture = textloader.load("./textures/z_11.png");
this.material = new THREE.MeshBasicMaterial({
map: this.texture,
transparent: true,
});

贴图
我们看到这个箭头只有一半,这是怎么回事呢,这是因为我们的管道是双面的,如果把他的uv展开,贴图贴上去我们能看到的的确只有一半
给大家画一个草图,大概就是这个样子
草图
如果我们想要在每一面都正常显示,那么我们应该将箭头在俩面贴上去,就是要下面这样镜像重复的效果
草图

1
2
3
4
5
6
7
8
9
10
const textloader = new THREE.TextureLoader();
this.texture = textloader.load("./textures/z_11.png");
this.texture.repeat.set(1, 2);
this.texture.wrapS = THREE.RepeatWrapping;
this.texture.wrapT = THREE.MirroredRepeatWrapping;

this.material = new THREE.MeshBasicMaterial({
map: this.texture,
transparent: true,
});

效果图
这样,我们的飞线就完成了,但是我们的飞线是静止的,我们需要让他动起来,我们可以使用gsap来让他动起来

1
2
3
4
5
6
gsap.to(this.texture.offset, {
x: -1,
duration: 1,
repeat: -1,
ease: "none",
});

使用shader实现飞线效果

同样的,我们需要新建这样的目录结构
目录结构

初始化代码

flylineshader

然后再flylineshader文件里面

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
import * as THREE from "three";
import gsap from "gsap";
import vertex from "@/shader/flyLine/vertex.glsl?raw";
import fragment from "@/shader/flyLine/fragment.glsl?raw";

export default class FlyLineShader {
constructor(position = { x: 0, z: 0 }, color = 0x00ffff) {
// 1/根据点生成曲线
let linePoints = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(position.x / 2, 4, position.z / 2),
new THREE.Vector3(position.x, 0, position.z),
];
// 创建曲线
this.lineCurve = new THREE.CatmullRomCurve3(linePoints);
const points = this.lineCurve.getPoints(1000);
// 2/创建几何顶点
this.geometry = new THREE.BufferGeometry().setFromPoints(points);
// 3/设置着色器材质
this.shaderMaterial = new THREE.ShaderMaterial({
uniforms: {},
vertexShader: vertex,
fragmentShader: fragment,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});

this.mesh = new THREE.Points(this.geometry, this.shaderMaterial);
}
remove() {
this.mesh.remove();
this.mesh.removeFromParent();
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
}

为了避免被遮挡住,我将transparent(透明度)开启了,depthWrite(深度写入关闭了)
接下来我们写简单的片元着色器和顶点着色器

片元着色器

1
2
3
void main(){
gl_FragColor = vec4(1,0,0,1);
}

顶点着色器

1
2
3
4
5
void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = 10.0;
}

效果图

实现飞线从大到小

要实现这个功能,就需要我们记录下点的位置,在js中添加以下代码

1
2
3
4
5
6
7
8
9
10
// 给每一个顶点设置属性
const aSizeArray = new Float32Array(points.length);
for (let i = 0; i < aSizeArray.length; i++) {
aSizeArray[i] = i;
}
// 设置几何体顶点属性
this.geometry.setAttribute(
"aSize",
new THREE.BufferAttribute(aSizeArray, 1)
);

在顶点着色器中添加以下代码

1
2
3
4
5
6
attribute float aSize;
void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = aSize*0.01;
}

此时的效果为,可以明确看到飞线不再是均匀的了
效果图

实现飞线只展示一部分效果

在顶点着色器中,我们设置一个vSize变量,然后将aSize-500的值传递给vSize,然后将vSize传递给片元着色器

1
2
3
4
5
6
7
8
9
attribute float aSize;
varying float vSize;

void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vSize = (aSize-500.0)*0.1;
gl_PointSize = vSize*0.01;
}

在片元着色器中

1
2
3
4
5
6
7
8
9
varying float vSize;
void main(){
if(vSize<0.0){
gl_FragColor = vec4(1,0,0,0);
}
else{
gl_FragColor = vec4(1,0,0,1);
}
}

这样就可以实现我们想要的只需要一部分飞线的需求了
效果图

飞线截面变成圆而不是长方形

效果图
通过这张图片我们可以清楚看到,这个飞线截面其实是正方形,我们需要将这个截面改造成圆形
在片元着色器中

1
2
3
4
5
6
7
8
9
10
11
 varying float vSize;
void main(){
float distanceToCenter = distance(gl_PointCoord,vec2(0.5,0.5));
float strength = 1.0 - (distanceToCenter*2.0);
if(vSize<0.0){
gl_FragColor = vec4(1,0,0,0);
}
else{
gl_FragColor = vec4(1,0,0,strength);
}
}

效果图

调整飞线的大小

我们可以看到如下视频,这个线因为远离我们就看起来特别的大,这样是不符合我们的需求的,我们需要将这个线的大小调整一下

在顶点着色器中根据相机视角进行大小的调整

1
2
3
4
5
6
7
8
9
attribute float aSize;
varying float vSize;

void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vSize = (aSize-500.0)*0.1;
gl_PointSize = -vSize/viewPosition.z;
}

这样,我们的飞线就看起来正常多了

让飞线动起来

我们可以想到,飞线就是从0-最高点的一个过程,我们需要让他一直动,就是前一条飞线结束的时候,另外一条飞线开始,那么就可以理解为这样的一个函数,就是理解为如何从右边这条线变成左边这条线,显示根据y轴翻转,然后再根据中心点折叠,类似于1-x这样的一条线
函数图
函数图
在js中,我们添加颜色和时间以及动画函数

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
import * as THREE from "three";
import gsap from "gsap";
import vertex from "@/shader/flyLine/vertex.glsl?raw";
import fragment from "@/shader/flyLine/fragment.glsl?raw";

export default class FlyLineShader {
constructor(position = { x: 0, z: 0 }, color = 0x00ffff) {
// 1/根据点生成曲线
let linePoints = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(position.x / 2, 4, position.z / 2),
new THREE.Vector3(position.x, 0, position.z),
];
// 创建曲线
this.lineCurve = new THREE.CatmullRomCurve3(linePoints);
const points = this.lineCurve.getPoints(1000);
// 2/创建几何顶点
this.geometry = new THREE.BufferGeometry().setFromPoints(points);

// 给每一个顶点设置属性
const aSizeArray = new Float32Array(points.length);
for (let i = 0; i < aSizeArray.length; i++) {
aSizeArray[i] = i;
}
// 设置几何体顶点属性
this.geometry.setAttribute(
"aSize",
new THREE.BufferAttribute(aSizeArray, 1)
);
// 3/设置着色器材质
this.shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: {
value: 0,
},
uColor: {
value: new THREE.Color(color),
},
uLength: {
value: points.length,
},
},
vertexShader: vertex,
fragmentShader: fragment,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});

this.mesh = new THREE.Points(this.geometry, this.shaderMaterial);

// 改变uTime来控制动画
gsap.to(this.shaderMaterial.uniforms.uTime, {
value: 1000,
duration: 2,
repeat: -1,
ease: "none",
});
}
remove() {
this.mesh.remove();
this.mesh.removeFromParent();
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
}

在片元着色器中,我们接收颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
varying float vSize;
uniform vec3 uColor;
void main(){
float distanceToCenter = distance(gl_PointCoord,vec2(0.5,0.5));
float strength = 1.0 - (distanceToCenter*2.0);

if(vSize<=0.0){
gl_FragColor = vec4(1,0,0,0);
}else{
gl_FragColor = vec4(uColor,strength);
}

}

在顶点着色器中

1
2
3
if(vSize<0.0){
vSize = vSize + uLength;
}

上面一段代码就是我们说的翻转逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

uniform float uTime;
uniform vec3 uColor;
uniform float uLength;
attribute float aSize;
varying float vSize;

void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vSize = (aSize-uTime);
if(vSize<0.0){
vSize = vSize + uLength;
}
vSize = (vSize-500.0)*0.1;

gl_PointSize = -vSize/viewPosition.z;
}

最终效果如下面这个视频展示

建筑物添加线框特效

这个我们之前做过类似的,详解就不说了,直接上代码,用three的线框函数即可

添加MeshLine类

1
2
3
4
5
6
7
8
9
10
import * as THREE from "three";
export default class MeshLine {
constructor(geometry) {
const edges = new THREE.EdgesGeometry(geometry);
this.material = new THREE.LineBasicMaterial({ color: 0xffffff });
const line = new THREE.LineSegments(edges, this.material);
this.geometry = edges;
this.mesh = line;
}
}

在createCity中调用

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
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import * as THREE from "three";
import scene from "../scene";
import modifyCityMaterial from "../modify/modifyCityMaterial";
import FlyLine from "./FlyLine";
import FlyLineShader from "./FlyLineShader";
import MeshLine from "./MeshLine";

export default function createCity() {
const gltfLoader = new GLTFLoader();
gltfLoader.load("./model/city.glb", (gltf) => {
gltf.scene.traverse((item) => {
if (item.type == "Mesh") {
const cityMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(0x0c0e33),
});
item.material = cityMaterial;
modifyCityMaterial(item);
if (item.name == "Layerbuildings") {
const meshLine = new MeshLine(item.geometry);
const size = item.scale.x * 1.001;
// 将缩放比例设置到线框中
meshLine.mesh.scale.set(size, size, size);
scene.add(meshLine.mesh);
}
}
});
scene.add(gltf.scene);
});
// // 添加飞线
// const flyLine = new FlyLine();
// scene.add(flyLine.mesh);
// // 添加飞线着色器
// const flyLineShader = new FlyLineShader({ x: -10, z: 0 });
// scene.add(flyLineShader.mesh);
}

线框效果图

光墙特效

光墙类

同样的我们新建一个光墙的类

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
import * as THREE from "three";
import gsap from "gsap";
import vertex from "@/shader/lightWall/vertex.glsl?raw";
import fragment from "@/shader/lightWall/fragment.glsl?raw";

export default class LightWall {
constructor(
radius = 5,
length = 2,
position = { x: 0, z: 0 },
color = 0xff0000
) {
this.geometry = new THREE.CylinderGeometry(
radius,
radius,
2,
32,
1,
true
);
this.material = new THREE.ShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment,
transparent: true,
side: THREE.DoubleSide,
});

this.mesh = new THREE.Mesh(this.geometry, this.material);
// 计算高度差,用于渐变
this.mesh.position.set(position.x, 1, position.z);
this.mesh.geometry.computeBoundingBox();

let { min, max } = this.mesh.geometry.boundingBox;
// 获取物体的高度差
let uHeight = max.y - min.y;
this.material.uniforms.uHeight = {
value: uHeight,
};

// 光墙动画
gsap.to(this.mesh.scale, {
x: length,
z: length,
duration: 1,
repeat: -1,
yoyo: true,
});
}

remove() {
this.mesh.remove();
this.mesh.removeFromParent();
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
}

片元着色器

在片元着色器中

1
2
3
4
5
6
7
8
varying vec3 vPosition;
uniform float uHeight;
void main(){
// 设置混合的百分比
float gradMix = (vPosition.y+uHeight/2.0)/uHeight;
gl_FragColor = vec4(1,1,0,1.0-gradMix);

}

顶点着色器

在顶点着色器中

1
2
3
4
5
6
7
varying vec3 vPosition;
void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vPosition = position;

}

调用生成

然后再createCity中调用

1
2
3
// 添加光墙
const lightWall = new LightWall();
scene.add(lightWall.mesh);

光墙效果视频

雷达效果

参考我们之前讲着色器的时候说过的雷达效果,这里我就复制过来了,不多阐述

雷达类

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
import * as THREE from "three";
import gsap from "gsap";
import vertex from "@/shader/lightRadar/vertex.glsl?raw";
import fragment from "@/shader/lightRadar/fragment.glsl?raw";

export default class LightRadar {
constructor(radius = 2, position = { x: 0, z: 0 }, color = 0xff0000) {
this.geometry = new THREE.PlaneGeometry(radius, radius);
this.material = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(color) },
uTime: {
value: 0,
},
},
vertexShader: vertex,
fragmentShader: fragment,
transparent: true,
side: THREE.DoubleSide,
});

this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.position.set(position.x, 1, position.z);
this.mesh.rotation.x = -Math.PI / 2;

gsap.to(this.material.uniforms.uTime, {
value: 1,
duration: 1,
repeat: -1,
ease: "none",
});
}

remove() {
this.mesh.remove();
this.mesh.removeFromParent();
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
}

顶点着色器

把uv传递给片元着色器

1
2
3
4
5
6
varying vec2 vUv;
void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vUv = uv;
}

片元着色器

这里稍微讲解一下,rotate2d是旋转矩阵,可以查阅(bookshader文档)[https://thebookofshaders.com/?lan=ch]得到

1
2
vec2 newUv = rotate2d(uTime*6.28) * (vUv-0.5);
newUv += 0.5;

这两句代码首先是传参要求是角度,所以就用了uTime*2PI,然后uv减去0.5是为了根据圆心计算得到旋转矩阵,newUv再加回去是为了保证uv的范围在0-1之间

1
2
3
float alpha =  1.0 - step(0.5,distance(newUv,vec2(0.5)));
float angle = atan(newUv.x-0.5,newUv.y-0.5);
float strength = (angle+3.14)/6.28;

这几行代码是计算出来的是一个圆形的渐变,step是阶梯函数,distance是计算距离,atan是计算角度,strength是计算角度的百分比,这里我用了一个三角函数,获取点到圆心的距离,所以我用了angle+PI/2,然后除以2PI,这样就可以得到0-1-0的循环了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
varying vec2 vUv;
uniform vec3 uColor;
uniform float uTime;

mat2 rotate2d(float _angle){
return mat2(cos(_angle),-sin(_angle),
sin(_angle),cos(_angle));
}


void main(){
vec2 newUv = rotate2d(uTime*6.28) * (vUv-0.5);
newUv += 0.5;
float alpha = 1.0 - step(0.5,distance(newUv,vec2(0.5)));
float angle = atan(newUv.x-0.5,newUv.y-0.5);
float strength = (angle+3.14)/6.28;
gl_FragColor =vec4(uColor,alpha*strength);
}

添加天空盒子和抗锯齿

添加天空盒子

再scene.js中添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 场景天空盒
const textureCubeLoader = new THREE.CubeTextureLoader().setPath("./textures/");
const textureCube = textureCubeLoader.load([
"1.jpg",
"2.jpg",
"3.jpg",
"4.jpg",
"5.jpg",
"6.jpg",
]);

scene.background = textureCube;
scene.environment = textureCube;

天空盒子

添加抗锯齿

我们可以看到线框的锯齿感有点强,我们在renderer.js中添加以下代码

1
2
3
4
5
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
// 抗锯齿
antialias: true,
});

可以明显感觉线框更加的细腻了
抗锯齿

封装3d警告标识和点击事件

我们都知道这个可以用three的精灵图来实现,里面的点击事件之前都讲过了,不多赘述

创建类

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
import * as THREE from "three";
import camera from "../camera";

export default class AlarmSprite {
constructor(type = "火警", position = { x: -1.8, z: 3 }, color = 0xffffff) {
const textureLoader = new THREE.TextureLoader();
const typeObj = {
火警: "./textures/tag/fire.png",
治安: "./textures/tag/jingcha.png",
电力: "./textures/tag/e.png",
};

const map = textureLoader.load(typeObj[type]);
this.material = new THREE.SpriteMaterial({
map: map,
color: color,
// blending: THREE.AdditiveBlending,
transparent: true,
depthTest: false,
});

this.mesh = new THREE.Sprite(this.material);

// 设置位置
this.mesh.position.set(position.x, 3.5, position.z);

// 封装点击事件
this.fns = [];

// 创建射线
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();

// 事件的监听
window.addEventListener("click", (event) => {
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -((event.clientY / window.innerHeight) * 2 - 1);

this.raycaster.setFromCamera(this.mouse, camera);

event.mesh = this.mesh;
event.alarm = this;

const intersects = this.raycaster.intersectObject(this.mesh);
if (intersects.length > 0) {
this.fns.forEach((fn) => {
fn(event);
});
}
});
}
onClick(fn) {
this.fns.push(fn);
}

remove() {
this.mesh.remove();
this.mesh.removeFromParent();
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
}

调用

1
2
3
4
5
6
// 添加警告标识
const alarmSprite = new AlarmSprite('火警', { x: 0, z: 0 });
scene.add(alarmSprite.mesh);
alarmSprite.onClick(function (e) {
console.log("警告", e);
});

标识封装

添加大屏

这里就省略了,css实在太多了,直接看代码吧
在style.css中

1
2
3
4
5
6
html{
font-size:calc(100vw / 19.2);
}
body{
font-size: 16px;
}

新建一个组件BigScreen.vue,并且在app.vue中引入

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
<template>
<div id="bigScreen">
<div class="header">Codesigner智慧城市管理系统平台</div>
<div class="main">
<div class="left">
<div class="cityEvent" v-for="(item, key) in props.dataInfo">
<h3>
<span>{{ item.name }}</span>
</h3>
<h1>
<img src="../assets/bg/bar.svg" class="icon" />
<span>{{ toFixInt(item.number) }}({{ item.unit }})</span>
</h1>
<div class="footerBoder"></div>
</div>
</div>
<div class="right">
<div class="cityEvent list">
<h3>
<span>事件列表</span>
</h3>
<ul>
<li
v-for="(item, i) in props.eventList"
:class="{ active: currentActive == i }"
@click="toggleEvent(i)"
>
<h1>
<div>
<img class="icon" :src="imgs[item.name]" />
<span> {{ item.name }} </span>
</div>
<span class="time"> {{ item.time }} </span>
</h1>
<p>{{ item.type }}</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>

<script setup>
import eventHub from "@/utils/eventHub";
import { ref } from "vue";
import dianli from "@/assets/bg/dianli.svg";
import fire from "@/assets/bg/fire.svg";
import jingcha from "@/assets/bg/jingcha.svg";
const props = defineProps(["dataInfo", "eventList"]);
const imgs = {
电力: dianli,
火警: fire,
治安: jingcha,
};

const toFixInt = (num) => {
return num.toFixed(0);
};

const currentActive = ref(null);
eventHub.on("spriteClick", (data) => {
// console.log(data);
currentActive.value = data.i;
});

const toggleEvent = (i) => {
currentActive.value = i;
eventHub.emit("eventToggle", i);
};
</script>

<style scoped>
#bigScreen {
width: 100vw;
height: 100vh;
position: fixed;
z-index: 100;

left: 0;
top: 0;
pointer-events: none;
display: flex;
flex-direction: column;
}

.header {
/* width: 1920px;
height: 100px; */

width: 19.2rem;
height: 1rem;
background-image: url(@/assets/bg/title.png);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
text-align: center;
color: rgb(226, 226, 255);
font-size: 0.4rem;
}

.main {
flex: 1;
width: 19.2rem;
display: flex;
justify-content: space-between;
}

.left {
width: 4rem;
/* background-color: rgb(255,255,255,0.5); */
background-image: url(@/assets/bg/line_img.png);
background-repeat: no-repeat;
background-size: contain;
background-position: right center;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.4rem 0;
}

.right {
width: 4rem;
/* background-color: rgb(255,255,255,0.5); */
background-image: url(@/assets/bg/line_img.png);
background-repeat: no-repeat;
background-size: contain;
background-position: left center;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.4rem 0;
}

.cityEvent {
position: relative;
width: 3.5rem;
/* height: 3rem; */
margin-bottom: 0.5rem;
background-image: url(@/assets/bg/bg_img03.png);
background-repeat: repeat;
}

.cityEvent::before {
width: 0.4rem;
height: 0.4rem;
position: absolute;
left: 0;
top: 0;
border-top: 4px solid rgb(34, 133, 247);
border-left: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.cityEvent::after {
width: 0.4rem;
height: 0.4rem;
position: absolute;
right: 0;
top: 0;
border-top: 4px solid rgb(34, 133, 247);
border-right: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}
.footerBorder {
position: absolute;
bottom: 0;
bottom: 0;
width: 3.5rem;
height: 0.4rem;
}
.footerBorder::before {
width: 0.4rem;
height: 0.4rem;
position: absolute;
left: 0;
top: 0;
border-bottom: 4px solid rgb(34, 133, 247);
border-left: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.footerBorder::after {
width: 0.4rem;
height: 0.4rem;
position: absolute;
right: 0;
top: 0;
border-bottom: 4px solid rgb(34, 133, 247);
border-right: 4px solid rgb(34, 133, 247);
content: "";
display: block;
}

.icon {
width: 40px;
height: 40px;
}

h1 {
color: #fff;
display: flex;
align-items: center;
padding: 0 0.3rem 0.3rem;
justify-content: space-between;
font-size: 0.3rem;
}
h3 {
color: #fff;
display: flex;
align-items: center;
padding: 0.3rem 0.3rem;
}

h1 > div {
display: flex;
align-items: center;
}
h1 span.time {
font-size: 0.2rem;
font-weight: normal;
}

.cityEvent li > p {
color: #eee;
padding: 0rem 0.3rem 0.3rem;
}
.list h1 {
padding: 0.1rem 0.3rem;
}
.cityEvent.list ul {
pointer-events: auto;
cursor: pointer;
}

.cityEvent li.active h1 {
color: red;
}
.cityEvent li.active p {
color: red;
}

ul,
li {
list-style: none;
}
</style>

在App.vue中

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
<script setup>
import Scene from './components/Scene.vue'
import BigScreen from './components/BigScreen.vue';
import { onMounted, reactive, ref } from "vue";
import { getSmartCityInfo, getSmartCityList } from "@/api/api";
import gsap from "gsap";
const dataInfo = reactive({
iot: { number: 0 },
event: { number: 0 },
power: { number: 0 },
test: { number: 0 },
});

onMounted(async () => {
changeInfo();
getEventList();
setInterval(() => {
changeInfo();
getEventList();
}, 10000);
});

const changeInfo = async () => {
let res = await getSmartCityInfo();
for (let key in dataInfo) {
dataInfo[key].name = res.data.data[key].name;
dataInfo[key].unit = res.data.data[key].unit;
gsap.to(dataInfo[key], {
number: res.data.data[key].number,
duration: 1,
});
}
};

const eventList = ref([]);
const getEventList = async () => {
let result = await getSmartCityList();
eventList.value = result.data.list;
};
</script>

<template>
<Scene :eventList="eventList"></Scene>
<BigScreen :dataInfo="dataInfo" :eventList="eventList"></BigScreen>
</template>

<style scoped>
</style>

Scene.vue

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<template>
<div class="scene" ref="sceneDiv"></div>
</template>

<script setup>
import { onMounted, ref, watch } from "vue";
import * as THREE from "three";
import gsap from "gsap";
// 导入场景
import scene from "@/three/scene";
// 导入相机
import camera from "@/three/camera";
// 导入控制器
import controls from "@/three/controls";
// 导入辅助坐标轴
import axesHelper from "@/three/axesHelper";
// 导入渲染器
import renderer from "@/three/renderer";
// 初始化调整屏幕
import "@/three/init";
// 导入添加物体函数
import createMesh from "@/three/createMesh";
// 导入每一帧的执行函数
import animate from "@/three/animate";
import AlarmSprite from "@/three/mesh/AlarmSprite";
import LightWall from "@/three/mesh/LightWall";
import FlyLineShader from "@/three/mesh/FlyLineShader";
import LightRadar from "@/three/mesh/LightRadar";
import eventHub from "@/utils/eventHub";

const props = defineProps(["eventList"]);
// 场景元素div
let sceneDiv = ref(null);
// 添加相机
scene.add(camera);
// 添加辅助坐标轴
scene.add(axesHelper);
// 创建物体
createMesh();

onMounted(() => {
sceneDiv.value.appendChild(renderer.domElement);
animate();
});
const eventListMesh = [];
let mapFn = {
火警: (position, i) => {
const lightWall = new LightWall(1, 2, position);
lightWall.eventListIndex = i;
scene.add(lightWall.mesh);
eventListMesh.push(lightWall);
},
治安: (position, i) => {
// 生成随机颜色
const color = new THREE.Color(
Math.random(),
Math.random(),
Math.random()
).getHex();
// 添加着色器飞线
const flyLineShader = new FlyLineShader(position, color);
flyLineShader.eventListIndex = i;
scene.add(flyLineShader.mesh);
eventListMesh.push(flyLineShader);
},
电力: (position, i) => {
// 添加雷达
const lightRadar = new LightRadar(2, position);
lightRadar.eventListIndex = i;
scene.add(lightRadar.mesh);
eventListMesh.push(lightRadar);
},
};

eventHub.on("eventToggle", (i) => {
eventListMesh.forEach((item) => {
if (item.eventListIndex === i) {
item.mesh.visible = true;
} else {
item.mesh.visible = false;
}
});
const position = {
x: props.eventList[i].position.x / 5 - 10,
y: 0,
z: props.eventList[i].position.y / 5 - 10,
};
// controls.target.set(position.x, position.y, position.z);
gsap.to(controls.target, {
duration: 1,
x: position.x,
y: position.y,
z: position.z,
});
});

watch(
() => props.eventList,
(val) => {
// console.log(val);
eventListMesh.forEach((item) => {
item.remove();
});
props.eventList.forEach((item, i) => {
const position = {
x: item.position.x / 5 - 10,
z: item.position.y / 5 - 10,
};
const alarmSprite = new AlarmSprite(item.name, position);
alarmSprite.onClick(() => {
// console.log(item.name, i);
eventHub.emit("spriteClick", { event: item, i });
});
alarmSprite.eventListIndex = i;
eventListMesh.push(alarmSprite);
scene.add(alarmSprite.mesh);
if (mapFn[item.name]) {
mapFn[item.name](position, i);
}
});
}
);
</script>

<style>
.scene {
width: 100vw;
height: 100vh;
position: fixed;
z-index: 100;
left: 0;
top: 0;
}
</style>

安装axios,新增api接口,用apifox模拟请求数据,我将生成的json文件放在了源码当中,需要自己去apifox导入模拟数据
apijs
apifox模拟数据

让兄弟组件监听,安装mitt之后,新建utils文件夹,新建eventHub.js
vue3兄弟组件传值监听

代码之中渲染部分我就不讲解了,着重讲一下业务逻辑
首先是BigScreen.vue

1
2
3
const toFixInt = (num) => {
return num.toFixed(0);
};

这一段代码是因为为了让数字更新一下避免小数捣乱样式,把数字变成了整数

1
2
3
4
5
const currentActive = ref(null);
eventHub.on("spriteClick", (data) => {
// console.log(data);
currentActive.value = data.i;
});

这一段代码是监听Scene.vue中点击精灵图的事件,当点击了某个精灵图,那么就将这个精灵图对应的右侧事件样式高亮

1
2
3
4
const toggleEvent = (i) => {
currentActive.value = i;
eventHub.emit("eventToggle", i);
};

这一段代码测试点击事件列表中的某个参数,将值传过去

接下来是Scene.vue

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
watch(
() => props.eventList,
(val) => {
// console.log(val);
eventListMesh.forEach((item) => {
item.remove();
});
props.eventList.forEach((item, i) => {
const position = {
x: item.position.x / 5 - 10,
z: item.position.y / 5 - 10,
};
const alarmSprite = new AlarmSprite(item.name, position);
alarmSprite.onClick(() => {
// console.log(item.name, i);
eventHub.emit("spriteClick", { event: item, i });
});
alarmSprite.eventListIndex = i;
eventListMesh.push(alarmSprite);
scene.add(alarmSprite.mesh);
if (mapFn[item.name]) {
mapFn[item.name](position, i);
}
});
}
);

这一段是监听父元素事件改变,在场景中对应添加精灵图

ok,智慧城市的内容我就说到这里了

上一篇:
前端web端处理支付对接
下一篇:
【可视化学习】30-智慧城市(二)