在React中使用Three.js实现全国铁路路网并附带流光效果

效果图:

整体思路:

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);
    
}

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值