threejs three-dimensional map big screen project sharing

This is a recent company project. The customer's needs are based on the data of the head office and subsidiaries, and a large screen for data display is developed. On both sides of the big screen are some charts showing data, and the middle part is a three-dimensional map of China. Click on a province on the map of China to drill down to the display of the provincial map. On the map, some data will be marked and information signs will be made. As shown below:

Data is desensitized
Data is desensitized
Data is desensitized

This article will share some technical principles.

2d chart

The 2d chart part is mainly developed through the echart chart, and also involves the display of some icon text. In this part, I believe that most front-end personnel know how to develop. What may be needed is that developers have good sensitivity to colors, fonts, etc., and can restore the design to the greatest extent.

Since everyone is familiar with it, no more details will be given.

3D map display

For the 3D map part in the middle. We generally have several ways to achieve.

  1. Modelers model parts of the map
  2. Generate 3D models from json data
  3. Produce 3D models from svg images.

Among them, method 1 can achieve the best effect. After all, the model is modeled manually, and the required effects can be adjusted by the smart hands of the modeler. However, the workload is relatively large, and it is necessary to establish a map of China and maps of various provinces. So we finally gave up this idea of ​​modeling.

Generate 3D maps from json data

The first thing to do is get the json data.
The json data of the map of China can be obtained through datav, refer to the following link
http://datav.aliyun.com/portal/school/atlas/area_selector

After getting the data, parse the json data, and then generate a map model through threejs' ExtrudeGeometry. The code looks like this:

 let jsonData = await (await fetch(jsonUrl)).json();
  // console.log(jsonData);
  let map = new dt.Group();
  if (type && type === "world") {
    jsonData.features = jsonData.features.filter(
      (ele) => ele.properties.name === "China"
    );
  }
  jsonData.features.forEach((elem, index) => {
    if (filter && filter(elem) == false) {
      return;
    }
    if (!elem.properties.name) {
      return;
    }
    // 定一个省份3D对象
    const province = new dt.Group();
    // 每个的 坐标 数组
    const coordinates = elem.geometry.coordinates;
    const color = COLOR_ARR[index % COLOR_ARR.length];
    // 循环坐标数组
    coordinates.forEach((multiPolygon, index) => {
      if (elem.properties.name == "海南省" && index > 0) {
        return;
      }
      if (elem.properties.name == "台湾省" && index > 0) {
        return;
      }
      if (elem.properties.name == "广东省" && index > 0) {
        return;
      }
      multiPolygon.forEach((polygon) => {
        const shape = new dt.Shape();

        let positions = [];
        for (let i = 0; i < polygon.length; i++) {
          let [x, y] = projection(polygon[i]);

          if (i === 0) {
            shape.moveTo(x, -y);
          }
          shape.lineTo(x, -y);

          positions.push(x, -y, 4);
        }

        const lineMaterial = new dt.LineBasicMaterial({
          color: "white",
        });
        const lineGeometry = new dt.LineXGeometry();
        // let attribute = new dt.BufferAttribute(new Float32Array(positions), 3);
        // lineGeometry.setAttribute("position", attribute);
        lineGeometry.setPositions(positions);

        const extrudeSettings = {
          depth: 4,
          bevelEnabled: false,
          bevelSegments: 5,
          bevelThickness: 0.1,
        };

        const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
        // console.log("geometyr", geometry);
        const material = new dt.StandardMaterial({
          metalness: 1,
          // color: color,
          map: texture,
          transparent: true,
        });

        let material1 = new dt.StandardMaterial({
          // polygonOffset: true,
          // polygonOffsetFactor: 1,
          // polygonOffsetUnits: 1,
          metalness: 1,
          roughness: 1,
          color: color, //"#3abcbd",
        });

        material1 = createSideShaderMaterial(material1);

        const mesh = new dt.Mesh(geometry, [material, material1]);
        if (index % 2 === 0) {
          // mesh.scale.set(1, 1, 1.2);
        }

        mesh.castShadow = true;
        mesh.receiveShadow = true;
        mesh._color = color;
        mesh.properties = elem.properties;
        if (!type) {
          province.add(mesh);
        }

        const matLine = new dt.LineXMaterial({
          polygonOffset: true,
          polygonOffsetFactor: -1,
          polygonOffsetUnits: -1,
          color: type === "world" ? "#00BBF4" : 0xffffff,
          linewidth: type === "world" ? 3.0 : 0.25, // in pixels
          vertexColors: false,
          dashed: false,
        });
        matLine.resolution.set(graph.width, graph.height);
        line = new dt.LineX(lineGeometry, matLine);
        line.computeLineDistances();
        province.add(line);
      });
    });

    // 将geo的属性放到省份模型中
    province.properties = elem.properties;
    if (elem.properties.centorid) {
      const [x, y] = projection(elem.properties.centorid);
      province.properties._centroid = [x, y];
    }

    map.add(province);

The json data of the map of China actually includes the data of each province.
The above code generates a map of China with outlines between provinces.
Among them, projection is a projection function, which converts latitude and longitude coordinates to plane coordinates, using the d3 library:

const projection = d3
  .geoMercator()
  .center([104.0, 37.5])
  .scale(80)
  .translate([0, 0]);

According to the design draft, the outline of the entire map of China needs to be generated. In this case, we first get world.json, and then only get the part of China, and use this part to generate the outline.

The final effect is as follows:

insert image description here

It can be seen that the map produced by json, the json data of the world map and the json data of the Chinese map, the edge fit is not high, so the outer edge outline and the map block cannot be well integrated.

Based on this, a new solution needs to be found.

Generate 3D maps from svg data

Since a designer provides the design draft, the designer can definitely provide the outline data of the map of China, as well as the outline data of each province inside. After getting the designed svg, analyze the svg path, then use ExtrudeGeometry to generate map block pairs, and use line to generate outlines.

 let childNodes = svg.childNodes;
  childNodes.forEach((child) => {
    readSVGPath(child, graph, group);
  });
  if (svg.tagName == "path") {
    const shape = getShapeBySvg(svg);
    // let shape = $d3g.transformSVGPath(pathStr);
    const extrudeSettings = {
      depth: 15,
      bevelEnabled: false,
      bevelSegments: 5,
      bevelThickness: 0.1,
    };

    const color = COLOR_ARR[parseInt(Math.random() * 3) % COLOR_ARR.length];
    const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
    let center = new dt.Vec3();
    // console.log(geometry.getBoundingBox().getCenter(center));
    // geometry.translate(-center.x, -center.y, -center.z);
    geometry.scale(1, -1, -1);
    geometry.computeVertexNormals();
    // console.log("geometry", geometry);
    const material = new dt.StandardMaterial({
      metalness: 1,
      // color: color,
      // visible: false,
      map: window.texture,
    });

    let material1 = new dt.StandardMaterial({
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      metalness: 1,
      roughness: 1,
      color: color, //"#3abcbd",
    });

    material1 = createSideShaderMaterial(material1);

    const mesh = new dt.Mesh(geometry, [material, material1]);
    group.add(mesh);

The code for parsing the svg path is as follows:

function getShapeBySvg(svg) {
  let pathStr = svg.getAttribute("d");
  let province = svg.getAttribute("province");
  let commonds = new svgpathdata.SVGPathData(pathStr).commands;

  const shape = new dt.Shape();
  let lastC, cmd, c;
  for (let i = 0; i < commonds.length; i++) {
    cmd = commonds[i];
    let relative = cmd.relative;

    if (relative) {
      c = copy(cmd);
      let x = cmd.x || 0;
      let y = cmd.y || 0;
      let lx = lastC.x || 0;
      let ly = lastC.y || 0;
      c.x = x + lx;
      c.y = y + ly;
      c.x1 = c.x1 + lx;
      c.x2 = c.x2 + lx;
      c.y1 = c.y1 + ly;
      c.y2 = c.y2 + ly;
    } else {
      c = cmd;
    }
    if (lastC) {
      let lx = lastC.x,
        ly = lastC.y;
      if (
        Math.hypot(lx - c.x, ly - c.y) < 0.2 &&
        province == "内蒙" &&
        [16, 32, 128, 64, 512, 4, 8].includes(c.type)
      ) {
        console.log(c.type);
        continue;
      }
    }
    if (c.type == 2) {
      shape.moveTo(c.x, c.y);
    } else if (c.type == 16) {
      shape.lineTo(c.x, c.y);
    } else if (c.type == 32) {
      shape.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
      // shape.lineTo(c.x, c.y);
    } else if (c.type == 128 || c.type == 64) {
      shape.quadraticCurveTo(c.x1 || c.x2, c.y1 || c.y2, c.x, c.y);
      // shape.lineTo(c.x, c.y);
    } else if (c.type == 512) {
      // shape.absellipse(c.x, c.y, c.rX, c.rY, 0, Math.PI * 2, true);
      shape.lineTo(c.x, c.y);
    } else if (c.type == 4) {
      c.y = lastC.y;
      shape.lineTo(c.x, lastC.y);
    } else if (c.type == 8) {
      c.x = lastC.x;
      shape.lineTo(lastC.x, c.y);
    } else if (c.type == 1) {
      // shape.closePath();
    } else {
      // console.log(c);
    }
    lastC = c;
  }
  return shape;
}

It involves the concept of relative positioning. The coordinates of a cmd are relative to the previous coordinates, not absolute positioning. This requires us to obtain absolute positioning coordinates by accumulating during parsing.

In addition, the type of cmd mainly includes:

  //   ARC: 512
  // CLOSE_PATH: 1
  // CURVE_TO: 32
  // DRAWING_COMMANDS: 1020
  // HORIZ_LINE_TO: 4
  // LINE_COMMANDS: 28
  // LINE_TO: 16
  // MOVE_TO: 2
  // QUAD_TO: 128
  // SMOOTH_CURVE_TO: 64
  // SMOOTH_QUAD_TO: 256
  // VERT_LINE_TO: 8

Correspond to it through Shape's moveTo, lineTo, bezierCurveTo, quadraticCurveTo, etc.
The final effect is as shown in the figure below:
insert image description here
It can be seen that the line is smoother, and the fit between the outer contour and the map block is higher.
This is the technical solution finally adopted by our project.

side gradient effect

From the renderings of the above two schemes, it can be seen that the side of the side map has a gradient effect, which is realized by customizing the shader of the threejs material. The general code is as follows:


function createSideShaderMaterial(material) {
  material.onBeforeCompile = function (shader, renderer) {
    // console.log(shader.fragmentShader);
    shader.vertexShader = shader.vertexShader.replace(
      "void main() {",
      "varying vec4 vPosition;\nvoid main() {"
    );
    shader.vertexShader = shader.vertexShader.replace(
      "#include <fog_vertex>",
      "#include <fog_vertex>\nvPosition=modelMatrix * vec4( transformed, 1.0 );"
    );

    shader.fragmentShader = shader.fragmentShader.replace(
      "void main() {",
      "varying vec4 vPosition;\nvoid main() {"
    );

    shader.fragmentShader = shader.fragmentShader.replace(
      "#include <transmissionmap_fragment>",
      `
      #include <transmissionmap_fragment>
      float z = vPosition.z;
      float s = step(2.0,z);
      vec3 bottomColor =  vec3(.0,1.,1.0);
    
      diffuseColor.rgb = mix(bottomColor,diffuseColor.rgb,s);
      // float r =  abs( 1.0 * (1.0 - s) + z  * (0.0  - s * 1.0) + s * 4.0) ;
      float r =  abs(z  * (1.0  - s * 2.0) + s * 4.0) ;
      diffuseColor.rgb *= pow(r, 0.5 + 2.0 * s);
      
      // float c = 
    `
    );
  };

  return material;
}

The dynamic change of the material is realized through the material.onBeforeCompile method, and then the gradient difference operation of the color is performed through the height of the z coordinate.

Textures for 3D maps

The effects achieved above are all simple colors. There is no texture effect, and the prototype provided by the designer has a gradient effect:

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-j7BuKd9p-1667965040240)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp /5f0c6a260f3647b991609a440ae85002~tplv-k3u1fbpfcp-watermark.image?)]

This requires our textures to resolve. But the texture is not simple, it involves the calculation of uv's offset and repeat. By calculating the boundingbox of the entire Chinese map, and setting the offset and repeat of the uv through the size and min value of the bongdingbox, the texture and model can be well mapped, as shown in the following code:

 let box = new dt.Box3();
 box.setFromObject(map);
 et size = new dt.Vec3(),
    center = new dt.Vec3();
console.log(box.getSize(size));
console.log(box.getCenter(center));
console.log(box);

texture.repeat.set(1 / size.x, 1 / size.y);
texture.offset.set(box.min.x / size.x, box.min.y / size.y);

In this way, the texture can be well aligned with the model, and there is little difference between the final effect and the design draft.

3D map icon label positioning

The icon positioning data on the picture is latitude and longitude, so the positioning degree needs to be converted into coordinates in 3D. Bilinear difference is used here. First obtain the latitude and longitude coordinates and three-dimensional coordinates of the upper left, upper right, lower left, and lower right points of the model, and then calculate the three-dimensional coordinates by combining the latitude and longitude values ​​of a specific point through the bilinear difference. This method is certainly not the most accurate, but it is the simplest. This method can be used if the accuracy of positioning is not high.

icon animation (APNG)

The animation of the icon is realized through the picture of apng. Analyze each frame of apng, and then draw it on the canvas as a sprite texture, and constantly refresh the content of the texture to achieve a dynamic effect. For the analysis of apng, there is an open source JavaScript analysis package on the Internet. Readers are free to do their own research, here is a reference link:

https://github.com/movableink/three-gif-loader

other

other aspects include

  1. The technical implementation of clicking the province to drill down is to hide the models of other provinces, display the model of the current province, and load the point data of the current province. The technical idea is relatively simple.
  2. The information such as the name and other information displayed by hovering the mouse is realized through the div to realize the information label, and the projection algorithm of converting the three-dimensional coordinates to the plane coordinates is used to calculate the position of the label. The code is as follows:
 getViewPosition(vector) {
    this.camera.updateMatrixWorld();
    var ret = new Vec3();
    // ret = this.projector.projectVector(vector, this._camera, ret);
    ret = vector.project(this.camera);
    ret.x = ret.x / 2 + 0.5;
    ret.y = -ret.y / 2 + 0.5;
    var point = {
      x: (this._canvas.width * ret.x) / this._pixelRatio,
      y: (this._canvas.height * ret.y) / this._pixelRatio,
      h: this._canvas.height,
    };
    return point;
  }

Summarize

The large screen of the 3D map shared above. There are not many technical points involved, including the following main technical points:

  • echart use
  • JSON analysis generates map projection projection
  • svg parsing to generate a 3D map model
  • Dynamic Material Modification
  • Texture offset and repeat algorithm, etc.
  • Longitude and latitude positioning, bilinear difference
  • Three-dimensional projection algorithm for converting three-dimensional coordinates to plane coordinates

In the end, the integration of multiple technologies produced the effect at the beginning of the article.

Data is desensitized
The more difficult one is the generation of the intermediate 3D map and the effect optimization scheme, readers who have similar needs can refer to it.

If you have good experience, welcome to communicate with me. Follow the official account "ITMan Biaoshu" to add the author's WeChat to communicate and receive more valuable articles in time.

Guess you like

Origin blog.csdn.net/netcy/article/details/127766732