Three.js 3D 地图特效与材质实现指南

目录

  1. 地形材质(MeshStandardMaterial)
  2. 飞线材质(ShaderMaterial)
  3. 侧边渐变材质(ShaderMaterial)
  4. 追光效果
  5. 飞线特效
  6. 旋转环装饰
  7. 高亮交互效果
  8. 呼吸浮动效果

一、地形材质(MeshStandardMaterial)

1.1 实现原理

使用 Three.js 的 PBR(物理正确渲染)材质,结合多张纹理实现真实感地形效果:

  • 漫反射纹理(diffuseMap):控制表面颜色
  • 位移纹理(displacementMap):控制地形起伏
  • 法线纹理(normalMap):控制表面细节和光照反应
  • 粗糙度纹理(roughnessMap):控制表面光滑程度

1.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>地形材质示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -50, 50);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(50, 50, 50);
    scene.add(directionalLight);

    const textureLoader = new THREE.TextureLoader();
    
    const textures = {
      diffuseMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
      displacementMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
      normalMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
      roughnessMap: textureLoader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg'),
    };

    textures.diffuseMap.wrapS = THREE.RepeatWrapping;
    textures.diffuseMap.wrapT = THREE.RepeatWrapping;
    textures.diffuseMap.repeat.set(2.8, 2.15);

    const terrainMaterial = new THREE.MeshStandardMaterial({
      color: '#0a1607',
      emissive: '#101d08',
      emissiveIntensity: 0.12,
      map: textures.diffuseMap,
      displacementMap: textures.displacementMap,
      displacementScale: 6.5,
      normalMap: textures.normalMap,
      normalScale: new THREE.Vector2(1.0, 1.0),
      roughnessMap: textures.roughnessMap,
      roughness: 0.94,
      metalness: 0.03,
      transparent: true,
      opacity: 0.85,
      side: THREE.DoubleSide,
      depthWrite: false,
    });

    const geometry = new THREE.SphereGeometry(20, 64, 64);
    const terrain = new THREE.Mesh(geometry, terrainMaterial);
    scene.add(terrain);

    function animate() {
      requestAnimationFrame(animate);
      terrain.rotation.y += 0.002;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

二、飞线材质(ShaderMaterial)

2.1 实现原理

使用自定义 ShaderMaterial 实现数据流向的动态飞线效果:

顶点着色器:传递 progress 属性到片元着色器

片元着色器:基于时间计算发光头位置,使用 smoothstep 创建三级渐变发光效果

  • head:发光头位置(fract 循环)
  • body:主体渐变
  • core:核心高亮

2.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>飞线材质示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -50, 50);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const flyLineShader = {
      uniforms: {
        uTime: { value: 0 },
        uDelay: { value: 0 },
        uSpeed: { value: 0.22 },
        uColor: { value: new THREE.Color('#F6FFD9') },
      },
      vertexShader: `
        varying float vProgress;
        attribute float progress;
        void main() {
          vProgress = progress;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform float uTime;
        uniform float uDelay;
        uniform float uSpeed;
        uniform vec3 uColor;
        varying float vProgress;

        void main() {
          float head = fract((uTime + uDelay) * uSpeed - vProgress);
          float body = smoothstep(0.24, 0.0, head);
          float core = smoothstep(0.035, 0.0, head);
          float alpha = body * 0.72 + core * 0.5;

          gl_FragColor = vec4(uColor, alpha);
        }
      `,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      depthWrite: false,
    };

    const curve = new THREE.QuadraticBezierCurve3(
      new THREE.Vector3(0, 0, 20),
      new THREE.Vector3(25, -25, 40),
      new THREE.Vector3(50, 0, 20)
    );

    const points = curve.getPoints(88);
    const geometry = new THREE.BufferGeometry().setFromPoints(points);

    const progress = new Float32Array(points.length);
    for (let i = 0; i < points.length; i++) {
      progress[i] = i / (points.length - 1);
    }
    geometry.setAttribute('progress', new THREE.BufferAttribute(progress, 1));

    const material = new THREE.ShaderMaterial(flyLineShader);
    const flyLine = new THREE.Line(geometry, material);
    scene.add(flyLine);

    function animate() {
      requestAnimationFrame(animate);
      material.uniforms.uTime.value += 0.016;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

三、侧边渐变材质(ShaderMaterial)

3.1 实现原理

用于地图 3D 侧边墙的渐变渲染,根据顶点的 Z 坐标(深度)混合三种颜色:

  • 底层:bottomColor → midColor(smoothstep 0~0.24)
  • 上层:lower → topColor(smoothstep 0.34~1)
  • 边缘发光:顶部边缘添加发光效果

3.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>侧边渐变材质示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -50, 50);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const sideGradientShader = {
      uniforms: {
        topColor: { value: new THREE.Color('#E8FF4F') },
        midColor: { value: new THREE.Color('#a8bc38') },
        bottomColor: { value: new THREE.Color('#101304') },
        alpha: { value: 0.85 },
        topZ: { value: 44 },
        bottomZ: { value: 20 },
      },
      vertexShader: `
        varying float vDepth;
        uniform float topZ;
        uniform float bottomZ;

        void main() {
          float depth = (position.z - bottomZ) / (topZ - bottomZ);
          vDepth = clamp(depth, 0.0, 1.0);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 midColor;
        uniform vec3 bottomColor;
        uniform float alpha;
        varying float vDepth;

        void main() {
          vec3 lower = mix(bottomColor, midColor, smoothstep(0.0, 0.24, vDepth));
          vec3 color = mix(lower, topColor, smoothstep(0.34, 1.0, vDepth));
          float edgeGlow = smoothstep(0.48, 1.0, vDepth);
          color += edgeGlow * topColor * 0.24;
          float finalAlpha = alpha * (0.46 + vDepth * 0.54);

          gl_FragColor = vec4(color, finalAlpha);
        }
      `,
      transparent: true,
      side: THREE.DoubleSide,
    };

    const boxGeometry = new THREE.BoxGeometry(30, 30, 24);
    const material = new THREE.ShaderMaterial(sideGradientShader);
    
    const topFaceMaterial = new THREE.MeshStandardMaterial({
      color: 0x2a2a3a,
      roughness: 0.8,
      metalness: 0.2
    });

    const materials = [
      material, material,
      topFaceMaterial, topFaceMaterial,
      material, material
    ];

    const box = new THREE.Mesh(boxGeometry, materials);
    box.position.z = 32;
    scene.add(box);

    function animate() {
      requestAnimationFrame(animate);
      box.rotation.y += 0.005;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

四、追光效果

4.1 实现原理

  1. 生成省份轮廓路径(网格化算法)
  2. 路径平滑处理(多次迭代)
  3. 构建多个 LineBasicMaterial 线段,透明度沿路径渐变
  4. 动画循环中更新每个线段的起点和终点位置,形成追逐效果

4.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>追光效果示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -60, 60);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const chaseLoop = [];
    const numPoints = 60;
    for (let i = 0; i < numPoints; i++) {
      const angle = (i / numPoints) * Math.PI * 2;
      const radius = 20 + Math.sin(angle * 3) * 5;
      chaseLoop.push(new THREE.Vector3(
        Math.cos(angle) * radius,
        Math.sin(angle) * radius,
        25
      ));
    }

    const distances = [];
    let totalDistance = 0;
    for (let i = 0; i < chaseLoop.length; i++) {
      const p1 = chaseLoop[i];
      const p2 = chaseLoop[(i + 1) % chaseLoop.length];
      totalDistance += p1.distanceTo(p2);
      distances.push(totalDistance);
    }

    const segmentCount = 34;
    const segments = [];
    const chaseGroup = new THREE.Group();

    for (let i = 0; i < segmentCount; i++) {
      const fade = 1 - i / segmentCount;
      const positions = new Float32Array(6);
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      const material = new THREE.LineBasicMaterial({
        color: '#ffffff',
        transparent: true,
        opacity: Math.pow(fade, 1.35) * 0.9,
        blending: THREE.AdditiveBlending,
        depthTest: false,
        depthWrite: false,
      });
      const line = new THREE.Line(geometry, material);
      line.frustumCulled = false;
      chaseGroup.add(line);
      segments.push({ geometry, material, positions });
    }

    scene.add(chaseGroup);

    function samplePoint(distance) {
      while (distance < 0) distance += totalDistance;
      while (distance > totalDistance) distance -= totalDistance;

      for (let i = 0; i < distances.length; i++) {
        if (distances[i] >= distance) {
          const prevDist = i === 0 ? 0 : distances[i - 1];
          const t = (distance - prevDist) / (distances[i] - prevDist);
          const p1 = chaseLoop[i];
          const p2 = chaseLoop[(i + 1) % chaseLoop.length];
          return p1.clone().lerp(p2, t);
        }
      }
      return chaseLoop[0].clone();
    }

    let time = 0;
    const chaseSpeed = 320;
    const tailLength = Math.max(90, totalDistance * 0.06);

    function animate() {
      requestAnimationFrame(animate);
      time += 0.016;

      const headDistance = (time * chaseSpeed) % totalDistance;
      const segmentLength = tailLength / segmentCount;

      segments.forEach((segment, index) => {
        const start = samplePoint(headDistance - index * segmentLength);
        const end = samplePoint(headDistance - (index + 0.88) * segmentLength);

        segment.positions[0] = start.x;
        segment.positions[1] = start.y;
        segment.positions[2] = start.z;
        segment.positions[3] = end.x;
        segment.positions[4] = end.y;
        segment.positions[5] = end.z;

        const fade = 1 - index / segments.length;
        segment.material.opacity = Math.pow(fade, 1.35) * 0.9;

        const positionAttribute = segment.geometry.getAttribute('position');
        positionAttribute.needsUpdate = true;
      });

      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

五、飞线特效

5.1 实现原理

  1. 确定源点(首都/省会)和目标点(其他城市)
  2. 使用 QuadraticBezierCurve3 创建贝塞尔曲线路径
  3. 双层结构:基线(静态)+ 流动线(动态)
  4. 流动线使用 ShaderMaterial 实现发光流动效果

5.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>飞线特效示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -80, 80);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const sourcePoint = new THREE.Vector3(0, 0, 20);

    const targets = [
      { name: '杭州', pos: new THREE.Vector3(30, -20, 20) },
      { name: '宁波', pos: new THREE.Vector3(45, -10, 20) },
      { name: '温州', pos: new THREE.Vector3(20, -40, 20) },
      { name: '嘉兴', pos: new THREE.Vector3(35, 5, 20) },
      { name: '湖州', pos: new THREE.Vector3(30, 15, 20) },
    ];

    const flyLineShader = {
      uniforms: {
        uTime: { value: 0 },
        uDelay: { value: 0 },
        uSpeed: { value: 0.3 },
        uColor: { value: new THREE.Color('#F6FFD9') },
      },
      vertexShader: `
        varying float vProgress;
        attribute float progress;
        void main() {
          vProgress = progress;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform float uTime;
        uniform float uDelay;
        uniform float uSpeed;
        uniform vec3 uColor;
        varying float vProgress;

        void main() {
          float head = fract((uTime + uDelay) * uSpeed - vProgress);
          float body = smoothstep(0.24, 0.0, head);
          float core = smoothstep(0.035, 0.0, head);
          float alpha = body * 0.72 + core * 0.5;

          gl_FragColor = vec4(uColor, alpha);
        }
      `,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      depthWrite: false,
    };

    targets.forEach((target, index) => {
      const midPoint = sourcePoint.clone().add(target.pos).multiplyScalar(0.5);
      midPoint.z = Math.max(28, sourcePoint.distanceTo(target.pos) * 0.15);

      const curve = new THREE.QuadraticBezierCurve3(
        sourcePoint.clone(),
        midPoint,
        target.pos.clone()
      );

      const points = curve.getPoints(88);
      const geometry = new THREE.BufferGeometry().setFromPoints(points);

      const progress = new Float32Array(points.length);
      for (let i = 0; i < points.length; i++) {
        progress[i] = i / (points.length - 1);
      }
      geometry.setAttribute('progress', new THREE.BufferAttribute(progress, 1));

      const material = new THREE.ShaderMaterial({
        ...flyLineShader,
        uniforms: {
          ...flyLineShader.uniforms,
          uDelay: { value: index * 0.5 },
        },
      });

      const flyLine = new THREE.Line(geometry, material);
      scene.add(flyLine);

      const baseLineGeometry = new THREE.BufferGeometry().setFromPoints(points);
      const baseLineMaterial = new THREE.LineBasicMaterial({
        color: 0xE8FF4F,
        transparent: true,
        opacity: 0.15,
      });
      const baseLine = new THREE.Line(baseLineGeometry, baseLineMaterial);
      scene.add(baseLine);
    });

    const sourceMarker = new THREE.Mesh(
      new THREE.SphereGeometry(2, 16, 16),
      new THREE.MeshBasicMaterial({ color: 0xE8FF4F })
    );
    sourceMarker.position.copy(sourcePoint);
    scene.add(sourceMarker);

    function animate() {
      requestAnimationFrame(animate);
      flyLineShader.uniforms.uTime.value += 0.016;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

六、旋转环装饰

6.1 实现原理

多层环形装饰元素,包含:

  • 软边缘环(RingMesh)
  • 弧线组(多个圆弧)
  • 刻度线(均匀分布的短线)
  • 整体旋转动画

6.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>旋转环装饰示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -100, 100);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const ringGroup = new THREE.Group();

    const softRingGeometry = new THREE.RingGeometry(356, 362, 192);
    const softRingMaterial = new THREE.MeshBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0.055,
      side: THREE.DoubleSide,
    });
    const softRing = new THREE.Mesh(softRingGeometry, softRingMaterial);
    softRing.rotation.x = -Math.PI / 2;
    ringGroup.add(softRing);

    const innerSoftRingGeometry = new THREE.RingGeometry(244, 248, 160);
    const innerSoftRingMaterial = new THREE.MeshBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0.035,
      side: THREE.DoubleSide,
    });
    const innerSoftRing = new THREE.Mesh(innerSoftRingGeometry, innerSoftRingMaterial);
    innerSoftRing.rotation.x = -Math.PI / 2;
    ringGroup.add(innerSoftRing);

    const arcRadii = [390, 316, 252];
    const arcMaterial = new THREE.LineBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0.24,
    });
    const dimArcMaterial = new THREE.LineBasicMaterial({
      color: 0xb5ca40,
      transparent: true,
      opacity: 0.15,
    });

    arcRadii.forEach((radius) => {
      for (let i = 0; i < 11; i++) {
        const startAngle = (i / 11) * Math.PI * 2;
        const endAngle = startAngle + Math.PI / 6;

        const arcCurve = new THREE.ArcCurve(0, 0, radius, startAngle, endAngle, false);
        const arcPoints = arcCurve.getPoints(32);
        const arcGeometry = new THREE.BufferGeometry().setFromPoints(arcPoints);
        const arcLine = new THREE.Line(arcGeometry, i % 2 === 0 ? arcMaterial : dimArcMaterial);
        arcLine.rotation.x = -Math.PI / 2;
        ringGroup.add(arcLine);
      }
    });

    const tickMaterial = new THREE.LineBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0.18,
    });

    for (let i = 0; i < 48; i++) {
      if (i % 4 === 0) continue;

      const angle = (i / 48) * Math.PI * 2;
      const innerRadius = 344;
      const outerRadius = 370;

      const startPoint = new THREE.Vector3(
        Math.cos(angle) * innerRadius,
        Math.sin(angle) * innerRadius,
        0
      );
      const endPoint = new THREE.Vector3(
        Math.cos(angle) * outerRadius,
        Math.sin(angle) * outerRadius,
        0
      );

      const tickGeometry = new THREE.BufferGeometry().setFromPoints([startPoint, endPoint]);
      const tickLine = new THREE.Line(tickGeometry, tickMaterial);
      tickLine.rotation.x = -Math.PI / 2;
      ringGroup.add(tickLine);
    }

    scene.add(ringGroup);

    function animate() {
      requestAnimationFrame(animate);
      ringGroup.rotation.z += 0.004;
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

七、高亮交互效果

7.1 实现原理

鼠标悬停时触发以下效果:

  1. 区域抬升(平滑过渡到目标高度)
  2. 侧边墙变色(颜色变亮)
  3. 高亮层透明度增加
  4. 地形材质变化(颜色、自发光、透明度)

所有状态变化均使用 lerp 插值实现平滑过渡。

7.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>高亮交互效果示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -50, 50);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(50, 50, 50);
    scene.add(directionalLight);

    const group = new THREE.Group();

    const baseMaterial = new THREE.MeshStandardMaterial({
      color: '#07100b',
      roughness: 0.94,
      metalness: 0.03,
      side: THREE.DoubleSide,
    });

    const highlightMaterial = new THREE.MeshBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0,
      blending: THREE.AdditiveBlending,
      depthTest: false,
      depthWrite: false,
    });

    const geometries = [];
    const colors = ['#07100b', '#0a1607', '#0d1c09'];
    
    for (let i = 0; i < 5; i++) {
      const geometry = new THREE.BoxGeometry(15, 15, 24);
      geometries.push(geometry);

      const material = baseMaterial.clone();
      material.color.set(colors[i % colors.length]);

      const box = new THREE.Mesh(geometry, material);
      box.position.set((i - 2) * 20, 0, 32);
      box.userData = {
        originalZ: 32,
        targetZ: 32,
        originalColor: material.color.clone(),
        isHighlighted: false,
      };
      group.add(box);

      const highlightBox = new THREE.Mesh(geometry, highlightMaterial.clone());
      highlightBox.position.copy(box.position);
      highlightBox.scale.set(1.02, 1.02, 1.02);
      highlightBox.userData = { isHighlight: true };
      group.add(highlightBox);
    }

    scene.add(group);

    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    container.addEventListener('mousemove', (event) => {
      mouse.x = (event.clientX / container.clientWidth) * 2 - 1;
      mouse.y = -(event.clientY / container.clientHeight) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(group.children);

      group.children.forEach((child) => {
        if (child.userData.isHighlight) return;

        const isHighlighted = intersects.some((intersect) => intersect.object === child);
        child.userData.isHighlighted = isHighlighted;
        
        if (isHighlighted) {
          child.userData.targetZ = 48;
          child.material.emissive.set(0xE8FF4F);
          child.material.emissiveIntensity = 0.72;
        } else {
          child.userData.targetZ = 32;
          child.material.emissive.set(0x000000);
          child.material.emissiveIntensity = 0;
        }
      });

      group.children.forEach((child) => {
        if (!child.userData.isHighlight) return;
        
        const targetBox = group.children.find((c) => 
          Math.abs(c.position.x - child.position.x) < 1 && 
          !c.userData.isHighlight
        );
        
        if (targetBox?.userData.isHighlighted) {
          child.material.opacity = 0.32;
        } else {
          child.material.opacity = 0;
        }
      });
    });

    function animate() {
      requestAnimationFrame(animate);

      group.children.forEach((child) => {
        if (child.userData.isHighlight) return;

        const lerpFactor = 0.18;
        child.position.z += (child.userData.targetZ - child.position.z) * lerpFactor;
        child.material.opacity += ((child.userData.isHighlighted ? 0.68 : 1) - child.material.opacity) * lerpFactor;

        group.children.forEach((highlightChild) => {
          if (!highlightChild.userData.isHighlight) return;
          if (Math.abs(highlightChild.position.x - child.position.x) < 1) {
            highlightChild.position.z = child.position.z;
          }
        });
      });

      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

八、呼吸浮动效果

8.1 实现原理

地图整体沿 Z 轴做正弦波动动画,产生呼吸般的浮动效果:

typescript 复制代码
mapGroup.position.z = basePosition.z + Math.sin(t * 0.55) * 2;

周期约 11.4 秒,振幅 2 单位。

8.2 完整 HTML 实现

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>呼吸浮动效果示例</title>
  <style>
    body { margin: 0; background: #0a0a0f; overflow: hidden; }
    #container { width: 100vw; height: 100vh; }
  </style>
</head>
<body>
  <div id="container"></div>

  <script type="importmap">
    {
      "imports": {
        "three": "https://unpkg.com/three@0.164.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.164.1/examples/jsm/"
      }
    }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x0a0a0f);

    const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.set(0, -60, 60);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(50, 50, 50);
    scene.add(directionalLight);

    const mapGroup = new THREE.Group();

    const geometry = new THREE.BoxGeometry(40, 30, 24);
    const material = new THREE.MeshStandardMaterial({
      color: '#07100b',
      roughness: 0.94,
      metalness: 0.03,
      side: THREE.DoubleSide,
    });

    const box = new THREE.Mesh(geometry, material);
    box.position.z = 32;
    mapGroup.add(box);

    const sideGeometry = new THREE.BoxGeometry(40, 30, 24);
    const sideMaterial = new THREE.MeshBasicMaterial({
      color: 0xE8FF4F,
      transparent: true,
      opacity: 0.3,
      side: THREE.DoubleSide,
    });
    const sideBox = new THREE.Mesh(sideGeometry, sideMaterial);
    sideBox.position.z = 32;
    sideBox.scale.set(1.05, 1.05, 1.05);
    mapGroup.add(sideBox);

    scene.add(mapGroup);

    const basePosition = mapGroup.position.clone();
    const floatAmplitude = 2;
    const floatFrequency = 0.55;

    let time = 0;

    function animate() {
      requestAnimationFrame(animate);
      time += 0.016;

      mapGroup.position.z = basePosition.z + Math.sin(time * floatFrequency) * floatAmplitude;

      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

九、总结

特效/材质 类型 核心技术
地形材质 MeshStandardMaterial PBR 渲染 + 多纹理
飞线材质 ShaderMaterial 自定义着色器 + 时间动画
侧边渐变材质 ShaderMaterial 深度渐变 + smoothstep
追光效果 LineBasicMaterial 路径采样 + 逐段更新
飞线特效 ShaderMaterial + Line 贝塞尔曲线 + 动态发光
旋转环装饰 RingMesh + Line 多层嵌套 + 旋转动画
高亮交互效果 MeshStandardMaterial Raycaster + lerp 插值
呼吸浮动效果 Group 正弦波动

所有特效均使用 Three.js 原生 API 实现,无需额外依赖,可直接复制到项目中使用。

相关推荐
angerdream2 小时前
手把手编写儿童手机远程监控App之vue3用 AI Agent生成菜单
前端
cidy_982 小时前
Git Pull 代码冲突后完整回退教程
前端
JING小白2 小时前
Day 1 重学Vue:响应式系统的“底层逻辑”变更,Vue2旧时代的终结与Vue3新时代的开启
前端·vue.js
张就是我1065922 小时前
一个 ZIP 文件,把 webshell 写到了不该在的地方
前端
张就是我1065922 小时前
SPIP 的一个漏洞:你以为过滤了,其实没过滤干净
前端
一tiao咸鱼2 小时前
我用 Claude 做了一个 AI 面试刷题系统,支持 DeepSeek / 阿里 / GPT 帮你打分
前端
掘金一周3 小时前
对车完全小白,不知买油买电还是买混动,求建议| 沸点周刊 7.2
前端·人工智能·后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十六):目录结构更新、完善 token 系统(AI 表示 token 入库无需加密?)
前端·后端·ai编程
程序me3 小时前
Prompt、Context、Harness、Loop 之后是什么? AI工程下一个半年的关键词
前端·后端·ai编程