效果图:
整体思路:
1.将全国铁路的geoJson数据,通过d3进行投影变换,将经纬度转为three.js中的空间坐标。得到坐标后,生成对应的路径样条曲线
2.构建自定义的BufferGeometry一RoadGeometry。它可以生成多条路径构成的路网几何体。
3.通过shader实现流光动画
数据处理
1.铁路GeoJson
全国铁路geoJson · FFMMCC/资源合集 - 码云 - 开源中国 (gitee.com)
2.安装d3库
npm i d3
3.数据处理
import * as THREE from "three";
import * as d3 from "d3";
useEffect(() => {
const fileLoader = new THREE.FileLoader();
fileLoader.load("/chinaTrain.json", (geoJson) => {
const lines = [];
const geoList = JSON.parse(geoJson)["features"];
// 投影
const aProjection = d3.geoMercator().scale(10).translate([0, 0]);
geoList.forEach((geo, index) => {
const line = {
data: geo.properties,
points: [],
geo: geo,
};
// 过滤调地铁数据
if (
geo.geometry &&
geo.geometry.coordinates &&
!geo.properties.NAME.includes("地铁")
) {
line.points = geo.geometry.coordinates.map((point) => {
// 坐标转换
const [x, y] = aProjection(point);
// line.points.push(new THREE.Vector3(x, 0,y));
return new THREE.Vector3(x, 0.11, y);
});
// 基于points生成铁路样条曲线
const curve = new THREE.CatmullRomCurve3(line.points);
if (curve.getLength() > 0.1) {
// 过滤长度过短的铁路
lines.push(curve);
}
}
});
// 这里是的自定义几何体与材质,后续会进一步讲解
reft.current.geometry = new RoadTestGeometry(lines, 40, 0.01);
reft.current.material = new RoadMaterial();
// debugger
});
构建BufferGeometry
import * as THREE from "three";
// THREE。THREE.TubeGeometry
class RoadTestGeometry extends THREE.BufferGeometry {
constructor(pathList, tubularSegments = 50, roadWidth = 0.01) {
super();
this.type = "RoadGeometry";
this.parameters = {
pathList: pathList,
};
/**
*
切线向量(Tangent):路径的方向,即路径在每一点的瞬时方向。
法线向量(Normal):垂直于切线向量的向量,用于确定几何体的正面方向。
副法线向量(Binormal):垂直于切线和法线的向量,形成完整的局部坐标系。
*/
let frames = null;
let path = new THREE.Curve();
const vertex = new THREE.Vector3();
let P = new THREE.Vector3();
// buffer
//顶点
const vertices = [];
// uv坐标
const uvs = [];
// 索引
const indicesList = [];
// 路径长度
const aLens = [];
// create buffer data
generateBufferData();
// build geometry
this.setIndex(indicesList);
this.setAttribute(
"position",
new THREE.Float32BufferAttribute(vertices, 3)
);
this.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
this.setAttribute("aLen", new THREE.Float32BufferAttribute(aLens, 1));
// functions
function generateBufferData() {
generatePath();
}
function generatePath() {
//offset为计算索引时,不同路径的索引偏移量
let offset = 0;
for (let i = 0; i < pathList.length; i++) {
path = pathList[i];
/**
* frames 包含Tangent,Normal,Binormal
切线向量(Tangent):路径的方向,即路径在每一点的瞬时方向。
法线向量(Normal):垂直于切线向量的向量,用于确定几何体的正面方向。
副法线向量(Binormal):垂直于切线和法线的向量,形成完整的局部坐标系。
*/
frames = path.computeFrenetFrames(tubularSegments, false);
for (let j = 0; j < tubularSegments; j++) {
generateSegment(j);
}
generateSegment(tubularSegments);
generateIndices(offset);
generateUVs();
generateLens(path.getLength());
offset = vertices.length / 3;
}
}
//生成每段路径的顶点
function generateSegment(i) {
P = path.getPointAt(i / tubularSegments, P);
const N = frames.normals[i];
const B = frames.binormals[i];
vertex.x = P.x + (roadWidth / 2) * B.x;
vertex.y = P.y;
vertex.z = P.z + (roadWidth / 2) * B.z;
vertices.push(vertex.x, vertex.y, vertex.z);
vertex.x = P.x - (roadWidth / 2) * B.x;
vertex.y = P.y;
vertex.z = P.z - (roadWidth / 2) * B.z;
vertices.push(vertex.x, vertex.y, vertex.z);
}
//生成每段路径的索引
function generateIndices(offset) {
for (let j = 0; j < tubularSegments; j++) {
const a = 2 * j + offset; // 当前段左边顶点
const b = 2 * j + 1 + offset; // 当前段右边顶点
const c = 2 * j + 2 + offset; // 下一段左边顶点
const d = 2 * j + 3 + offset; // 下一段右边顶点
// faces
indicesList.push(a, b, d); // 三角形 (a, b, d)
indicesList.push(a, d, c); // 三角形 (a, d, c)
}
}
//生成每段路径的uv
function generateUVs() {
for (let i = 0; i <= tubularSegments; i++) {
uvs.push(i / tubularSegments, 0);
uvs.push(i / tubularSegments, 1);
}
}
//生成每段路径的长度
function generateLens(len) {
for (let i = 0; i <= tubularSegments; i++) {
aLens.push(len);
aLens.push(len);
}
}
}
}
export default RoadTestGeometry;
流光shader实现
1.安装three-custom-shader-material
npm i three-custom-shader-material
three-custom-shader-material
是一个用于扩展和定制 Three.js 标准材质的库,它允许开发者在不从头编写着色器的情况下,通过插入自定义着色器代码来实现复杂的视觉效果。这个库使得在保持原有材质特性(如光照、阴影)的基础上进行高级渲染效果的开发变得更加容易和灵活。适用于那些希望在标准材质基础上添加特殊效果的场景。这里是它的官方文档
three-custom-shader-material - npm (npmjs.com)
总而言之,这个就是个快捷自定义材质的库。
2.编写shader,自定义材质
import CustomShaderMaterial from "three-custom-shader-material/vanilla";
import * as THREE from "three";
import vs from "./shader/sbpk-china-traffic.vs.glsl";
import fs from "./shader/sbpk-china-traffic.fs.glsl";
export default class RoadMaterial extends CustomShaderMaterial {
constructor() {
super({
baseMaterial: THREE.MeshBasicMaterial,
color: "blue",
uniforms: {
uTime:{value:0}
},
depthWrite: false, // 禁用深度写入
vertexShader: vs,
fragmentShader: fs,
transparent: true
});
}
}
sbpk-china-traffic.vs(顶点着色器代码)
varying vec2 vUv;
varying float r;
varying float vOffset;
float N21(vec2 p)
{
p=fract(p*vec2(233.34,851.73));
p+=dot(p,p+237.45);
return fract(p.x*p.y);
}
attribute float aLen;
void main(){
vUv=uv;
vOffset=N21(vec2(aLen,aLen));
r=aLen;
}
sbpk-china-traffic.fs.glsl(片元着色器代码)
varying vec2 vUv;
uniform float uTime;
varying float vOffset;
varying float r;
vec3 hsv2rgb(float h,float s,float v)
{
vec4 t=vec4(1.,2./3.,1./3.,3.);
vec3 p=abs(fract(vec3(h)+t.xyz)*6.-vec3(t.w));
return v*mix(vec3(t.x),clamp(p-vec3(t.x),0.,1.),s);
}
void main(){
// float strength=sin(uTime);
// strength=strength*.5+.5;
// if(r>1.0)
// discard;
vec3 baseColor=vec3(0.098, 0.0, 1.0);
vec2 oUV=vUv;
float len=.2;
float h=mix(.5,.65,length(vUv));
vec3 vColor=hsv2rgb(h,1.,1.);
float start=.25;
float end=.75;
float blur=.2;
float step1=smoothstep(start-blur,start+blur,oUV.y);
float step2=smoothstep(end+blur,end-blur,oUV.y);
float alhp=step1*step2;
vColor=mix(baseColor,vColor,(1.-fract(vUv.x+vOffset+uTime*0.5)));
csm_DiffuseColor=vec4(vColor,alhp);
// csm_DiffuseColor=vec4(vec3(1.0,1.0,0.5),1.0);
}