学习threejs,结合anime.js打造炫酷文字粒子星空秀

👨‍⚕️ 主页: gis分享者

👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅!

👨‍⚕️ 收录于专栏:threejs gis工程师


文章目录

  • 一、🍀前言
    • [1.1 ☘️anime.js 动画库](#1.1 ☘️anime.js 动画库)
      • [1.1.1 ☘️核心特性](#1.1.1 ☘️核心特性)
      • [1.1.2 ☘️安装与引入](#1.1.2 ☘️安装与引入)
      • [1.1.3 ☘️使用示例](#1.1.3 ☘️使用示例)
  • 二、🍀结合anime.js打造炫酷文字粒子星空秀
    • [1. ☘️实现思路](#1. ☘️实现思路)
    • [2. ☘️代码样例](#2. ☘️代码样例)

一、🍀前言

本文详细介绍如何基于threejs在三维场景中结合anime.js打造炫酷文字粒子星空秀。亲测可用。希望能帮助到您。一起学习,加油!加油!

1.1 ☘️anime.js 动画库

Anime.js 是一个轻量级(核心文件仅约10KB)的 JavaScript 动画库,专注于为网页元素提供高性能、灵活的动画效果。

anime 官网

1.1.1 ☘️核心特性

广泛兼容性

支持 CSS 属性、DOM 属性、SVG 属性及 JavaScript 对象属性的动画,覆盖从简单位移到复杂路径变形(如 SVG 路径动画)的多种场景。

简洁 API

通过 anime() 函数快速创建动画,参数配置直观,适合快速开发。

高级功能

提供时间轴(Timeline)、交错动画(Stagger)、拖拽交互(Draggable)、响应式动画(Scope API)等工具,满足复杂动画需求。

性能优化

优先使用 transform 和 opacity 等不触发重排的属性,确保动画流畅性。

1.1.2 ☘️安装与引入

CDN 引入:在 HTML 文件中直接添加脚本标签:

javascript 复制代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>

通过 npm 安装 animejs:

javascript 复制代码
npm install animejs

随后在代码中导入:

javascript 复制代码
import anime from 'animejs';

1.1.3 ☘️使用示例

元素平移与旋转:

javascript 复制代码
<div class="box" style="width: 100px; height: 100px; background-color: blue;"></div>
<script>
  anime({
    targets: '.box', // 目标元素选择器
    translateX: 250, // X轴平移距离
    translateY: 250, // Y轴平移距离
    rotate: '1turn', // 旋转一周
    duration: 1000, // 动画持续时间(毫秒)
    easing: 'easeInOutQuad' // 缓动函数
  });
</script>

效果:蓝色方块从左上角平滑移动到右下角,同时旋转一周。

颜色渐变与缩放:

javascript 复制代码
<div class="circle" style="width: 100px; height: 100px; background-color: red; border-radius: 50%; opacity: 0;"></div>
<script>
  anime({
    targets: '.circle',
    opacity: [0, 1], // 透明度从0到1
    scale: [0.5, 1], // 缩放比例从0.5到1
    duration: 1000,
    loop: true, // 循环播放
    direction: 'alternate' // 动画方向交替(正放→倒放→正放...)
  });
</script>

效果:红色圆形淡入并放大至原始大小,循环执行且方向交替。

时间轴(Timeline):编排多个动画按顺序执行。

javascript 复制代码
const timeline = anime.timeline({ loop: true });
timeline
  .add({ targets: '.box1', scale: 1.5, duration: 500 })
  .add({ targets: '.box2', rotate: '1turn', duration: 500 }, '-200'); // 延迟200ms启动

效果:.box1 放大后,.box2 延迟200ms开始旋转,整体循环播放。

交错动画(Stagger):为一组元素添加延迟效果。

javascript 复制代码
anime({
  targets: '.items', // 假设有多个.items元素
  translateX: 100,
  delay: anime.stagger(100) // 每个元素间隔100ms启动
});

效果:多个元素依次向右移动,形成序列感。

SVG 路径动画:

javascript 复制代码
// 假设存在SVG路径元素 <path class="my-path" d="M0,0 L100,0" />
anime.setDashoffset(document.querySelector('.my-path')); // 初始化路径偏移
anime({
  targets: '.my-path',
  strokeDashoffset: [anime.setDashoffset, 0], // 从当前偏移量动画到0
  duration: 1500
});

效果:SVG线条从无到有逐渐绘制完成。

生命周期回调:

javascript 复制代码
anime({
  targets: '.box',
  translateX: 100,
  begin: () => console.log('动画开始'), // 动画启动时触发
  update: (anim) => console.log(`进度: ${anim.progress}%`), // 动画进行时触发
  complete: () => console.log('动画完成') // 动画结束时触发
});

动画控制:

javascript 复制代码
const myAnimation = anime({ /* 参数配置 */ });
myAnimation.pause(); // 暂停动画
myAnimation.play(); // 继续播放
myAnimation.reverse(); // 倒放动画
myAnimation.restart(); // 重新开始

二、🍀结合anime.js打造炫酷文字粒子星空秀

1. ☘️实现思路

集成了 Three.js(渲染)、anime.js (动画库)打造炫酷文字粒子星空秀。具体代码参考代码样例。可以直接运行。

2. ☘️代码样例

html 复制代码
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>打造炫酷文字粒子星空秀</title>
    <style>
        :root {
            --red: hsl(4, 70%, 50%);
        }
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            position: absolute;
            overflow: hidden;
            width: 100%;
            height: 100%;
            background: #000;
            display: flex;
            justify-content: center;
            align-items: center;
            font-family: "Arial Black", sans-serif;
        }
        .text-container {
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 15px;
        }
        .letter {
            position: relative;
            width: 100px;
            height: 120px;
        }
        .letter.dot {
            width: 30px;
        }
        .particle {
            position: absolute;
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: radial-gradient(
                    circle at 35% 35%,
                    rgba(255, 255, 255, 0.7),
                    rgba(255, 120, 180, 0.8) 20%,
                    rgba(255, 80, 120, 0.7) 35%,
                    rgba(200, 40, 80, 0.6) 50%,
                    rgba(150, 20, 60, 0.4) 70%,
                    transparent
            );
            mix-blend-mode: screen;
            will-change: transform;
            opacity: 0.95;
            box-shadow: 0 0 15px rgba(255, 100, 150, 0.6), 0 0 30px rgba(255, 80, 120, 0.4), 0 0 45px rgba(255, 60, 100, 0.2),
            inset -2px -2px 4px rgba(100, 0, 50, 0.3), inset 2px 2px 4px rgba(255, 255, 255, 0.5);
            filter: contrast(1.1) brightness(1);
        }
        .particle::before {
            content: "";
            position: absolute;
            top: 15%;
            left: 15%;
            width: 25%;
            height: 25%;
            background: radial-gradient(circle, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.3) 40%, transparent 70%);
            border-radius: 50%;
            filter: blur(0.3px);
        }
        .particle::after {
            content: "";
            position: absolute;
            bottom: 15%;
            right: 15%;
            width: 15%;
            height: 15%;
            background: radial-gradient(circle, rgba(255, 200, 255, 0.3), rgba(200, 150, 255, 0.15) 50%, transparent 70%);
            border-radius: 50%;
            filter: blur(0.5px);
        }
    </style>
</head>
<body>
<div class="text-container" id="textContainer"></div>
<!-- Three.js 用于3D行星 -->
<script src="https://unpkg.com/three@0.155.0/build/three.min.js"></script>
<!-- 行星背景容器 -->
<canvas id="planetCanvas" style="position: fixed; top: 0; left: 0; z-index: -1; pointer-events: none"></canvas>
<script type="module">
  import { animate, createTimeline, createTimer, stagger, utils } from "https://esm.sh/animejs";
  const textContainer = document.getElementById("textContainer");
  const viewport = { w: window.innerWidth * 0.5, h: window.innerHeight * 0.5 };
  const cursor = { x: 0, y: 0 };
  let allParticles = [];
  let originalPositions = [];
  // 各字符的粒子坐标(改进版)
  const letterPatterns = {
    a: [
      // 顶点
      [50, 35],
      // 左斜线
      [45, 45],
      [40, 55],
      [35, 65],
      [30, 75],
      [25, 85],
      [20, 90],
      // 右斜线
      [55, 45],
      [60, 55],
      [65, 65],
      [70, 75],
      [75, 85],
      [80, 90],
      // 横线
      [30, 70],
      [40, 70],
      [50, 70],
      [60, 70],
      [70, 70],
    ],
    n: [
      // 左竖线
      [20, 37],
      [20, 45],
      [20, 55],
      [20, 65],
      [20, 75],
      [20, 85],
      [20, 90],
      // 右竖线
      [70, 37],
      [70, 45],
      [70, 55],
      [70, 65],
      [70, 75],
      [70, 85],
      [70, 90],
      // 上曲线
      [20, 37],
      [30, 35],
      [40, 34],
      [50, 34],
      [60, 35],
      [70, 37],
    ],
    i: [
      // 上点
      [45, 20],
      [50, 20],
      [55, 20],
      // 竖线
      [50, 37],
      [50, 45],
      [50, 55],
      [50, 65],
      [50, 75],
      [50, 85],
      [50, 90],
    ],
    m: [
      // 左竖线
      [15, 37],
      [15, 45],
      [15, 55],
      [15, 65],
      [15, 75],
      [15, 85],
      [15, 90],
      // 中竖线
      [45, 37],
      [45, 45],
      [45, 55],
      [45, 65],
      [45, 75],
      [45, 85],
      [45, 90],
      // 右竖线
      [75, 37],
      [75, 45],
      [75, 55],
      [75, 65],
      [75, 75],
      [75, 85],
      [75, 90],
      // 左山(连接部分)
      [15, 37],
      [22, 35],
      [30, 34],
      [38, 35],
      [45, 37],
      // 右山(连接部分)
      [45, 37],
      [52, 35],
      [60, 34],
      [68, 35],
      [75, 37],
    ],
    e: [
      // 中央横线(最重要)
      [20, 60],
      [28, 60],
      [36, 60],
      [44, 60],
      [52, 60],
      [60, 60],
      [68, 60],
      // 上部打开的C形
      [68, 45],
      [60, 40],
      [50, 37],
      [40, 35],
      [30, 37],
      [22, 40],
      [20, 45],
      // 右上连接线(e的特征)
      [68, 45],
      [70, 50],
      [70, 55],
      [68, 60],
      // 左竖线(上半部分)
      [20, 45],
      [20, 50],
      [20, 55],
      [20, 60],
      // 左竖线(下半部分)
      [20, 60],
      [20, 65],
      [20, 70],
      [20, 75],
      [20, 80],
      [20, 85],
      // 下部打开的C形(更低)
      [20, 85],
      [22, 88],
      [30, 90],
      [40, 91],
      [50, 91],
      [60, 90],
      [68, 88],
      [72, 85],
    ],
    ".": [
      // 点(小)
      [12, 85],
      [15, 88],
      [12, 91],
    ],
    j: [
      // 上点
      [45, 20],
      [50, 20],
      [55, 20],
      // 竖线
      [50, 37],
      [50, 45],
      [50, 55],
      [50, 65],
      [50, 75],
      [50, 85],
      [50, 90],
      // 下曲线(钩)
      [45, 95],
      [35, 97],
      [25, 95],
      [20, 90],
      [18, 80],
    ],
    s: [
      // 上曲线(右开)- 与下方空白一致
      [65, 37],
      [55, 35],
      [45, 34],
      [35, 34],
      [25, 35],
      [20, 37],
      [18, 42],
      // 左侧竖线(上半部分)
      [18, 47],
      [18, 52],
      // 中央横线(右向)
      [20, 57],
      [30, 58],
      [40, 59],
      [50, 60],
      [60, 61],
      [65, 63],
      // 右侧竖线(下半部分)
      [70, 66],
      [70, 71],
      [70, 76],
      // 下曲线(左开)- 延伸更低
      [68, 81],
      [60, 86],
      [50, 89],
      [40, 90],
      [30, 90],
      [25, 89],
      [20, 87],
      [18, 84],
      // 最底部对齐其他字
      [22, 90],
      [30, 91],
      [40, 91],
      [50, 91],
      [60, 91],
      [65, 90],
    ],
  };
  const text = "anime.js";
  let currentX = 0;
  // 创建字符
  text.split("").forEach((char, charIndex) => {
    const letterEl = document.createElement("div");
    letterEl.classList.add("letter");
    if (char === ".") {
      letterEl.classList.add("dot");
    }
    const pattern = letterPatterns[char];
    if (!pattern) return;
    pattern.forEach((pos, i) => {
      const particle = document.createElement("div");
      particle.classList.add("particle");
      // 彩虹渐变
      const totalIndex = allParticles.length;
      const hue = (totalIndex / 150) * 360; // 按粒子总数分配彩虹
      const saturation = 100;
      const lightness = 50 + Math.random() * 15;
      // 立体彩虹渐变背景
      particle.style.background = `
          radial-gradient(circle at 35% 35%,
            hsla(${hue}, 100%, 90%, 0.7),
            hsla(${hue}, ${saturation}%, 70%, 0.8) 15%,
            hsla(${hue}, ${saturation}%, ${lightness}%, 0.85) 30%,
            hsla(${hue}, ${saturation}%, ${lightness - 10}%, 0.7) 50%,
            hsla(${hue}, ${saturation - 10}%, ${lightness - 20}%, 0.5) 70%,
            hsla(${hue}, ${saturation - 20}%, ${lightness - 30}%, 0.3) 85%,
            transparent
          )
        `;
      // 彩虹可见的阴影和高光
      particle.style.boxShadow = `
          0 0 15px hsla(${hue}, 100%, ${lightness}%, 0.7),
          0 0 25px hsla(${hue}, 100%, ${lightness - 5}%, 0.5),
          0 0 35px hsla(${hue}, 90%, ${lightness - 10}%, 0.3),
          inset -1px -1px 3px hsla(${hue + 180}, 60%, 25%, 0.4),
          inset 2px 2px 4px hsla(${hue}, 70%, 85%, 0.6)
        `;
      particle.style.filter = `contrast(1.1) brightness(1.05) saturate(1.3)`;
      letterEl.appendChild(particle);
      allParticles.push(particle);
      // 保存原始位置
      originalPositions.push({ x: 0, y: 0, letterIndex: charIndex });
      // 设置初始位置
      utils.set(particle, {
        left: pos[0] + "px",
        top: pos[1] + "px",
        translateX: "-50%",
        translateY: "-50%",
        scale: 0.8 + Math.random() * 0.4,
        opacity: 0.8 + Math.random() * 0.2,
        x: 0,
        y: 0,
      });
    });
    textContainer.appendChild(letterEl);
  });
  console.log(`生成 ${allParticles.length} 个 anime.js 粒子`);
  // 脉冲动画
  const pulse = () => {
    animate(allParticles, {
      scale: [1, 1.5, 1],
      opacity: [0.8, 1, 0.8],
      duration: 1000,
      delay: stagger(10),
      ease: "inOutQuad",
    });
  };
  // 主循环 - 按字母顺序排列
  const mainLoop = createTimer({
    frameRate: 15,
    onUpdate: () => {
      animate(allParticles, {
        x: cursor.x,
        y: cursor.y,
        delay: stagger(30, { from: "first" }), // 从第一个字母开始
        duration: 600, // 固定时间移动
        ease: "outQuart", // 平滑移动
        composition: "blend",
      });
    },
  });
  // 自动移动动画
  const autoMove = createTimeline()
    .add(
      cursor,
      {
        x: [-viewport.w * 0.45, viewport.w * 0.45],
        modifier: (x) => x + Math.sin(mainLoop.currentTime * 0.0007) * viewport.w * 0.5,
        duration: 3000,
        ease: "inOutExpo",
        alternate: true,
        loop: true,
        onBegin: pulse,
        onLoop: pulse,
      },
      0
    )
    .add(
      cursor,
      {
        y: [-viewport.h * 0.45, viewport.h * 0.45],
        modifier: (y) => y + Math.cos(mainLoop.currentTime * 0.00012) * viewport.h * 0.5,
        duration: 1000,
        ease: "inOutQuad",
        alternate: true,
        loop: true,
      },
      0
    );
  // 手动操作定时器 - 暂停动画
  const manualMovementTimeout = createTimer({
    duration: 2000,
    onComplete: () => {
      // 在光标位置停止
    },
  });
  // 鼠标跟随
  const followPointer = (e) => {
    const event = e.type === "touchmove" ? e.touches[0] : e;
    cursor.x = event.pageX - window.innerWidth / 2;
    cursor.y = event.pageY - window.innerHeight / 2;
    autoMove.pause();
    manualMovementTimeout.restart();
  };
  document.addEventListener("mousemove", followPointer);
  document.addEventListener("touchmove", followPointer);
  // 初始动画
  animate(allParticles, {
    opacity: [0, 0.9],
    scale: [0, 1],
    translateX: [stagger([-100, 100]), 0],
    translateY: [stagger([-100, 100]), 0],
    duration: 2000,
    delay: stagger(8, { from: "first" }),
    ease: "outElastic(1, 0.6)",
    complete: () => {
      pulse();
    },
  });
  // 持续闪烁动画
  allParticles.forEach((particle, i) => {
    animate(particle, {
      opacity: [0.9, 1, 0.9],
      scale: [1, 1.1, 1],
      duration: 2500 + Math.random() * 2000,
      delay: Math.random() * 2000,
      loop: true,
      ease: "inOutSine",
    });
  });
  // Three.js 初始化行星背景
  const canvas = document.getElementById("planetCanvas");
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha: true,
    antialias: true,
    powerPreference: "high-performance",
  });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.0;
  // 地球几何体和材质(着色器)
  const earthGeometry = new THREE.SphereGeometry(3, 256, 256);
  // NASA 高分辨率地球纹理加载器
  const textureLoader = new THREE.TextureLoader();
  // 高分辨率地球纹理(8K) - 带备用纹理
  const earthDayTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/earth-blue-marble.jpg", undefined, undefined, () =>
    textureLoader.load("https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73909/world.topo.bathy.200412.3x5400x2700.jpg")
  );
  const earthNightTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/earth-night.jpg", undefined, undefined, () =>
    textureLoader.load("https://eoimages.gsfc.nasa.gov/images/imagerecords/144000/144898/BlackMarble_2016_3km.jpg")
  );
  const earthCloudsTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/earth-water.png", undefined, undefined, () =>
    textureLoader.load("https://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57747/cloud_combined_2048.jpg")
  );
  const earthBumpTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/earth-topology.png", undefined, undefined, () =>
    textureLoader.load("https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73934/gebco_08_rev_elev_A2_grey_geo.tif")
  );
  // 设置纹理
  [earthDayTexture, earthNightTexture, earthCloudsTexture, earthBumpTexture].forEach((texture) => {
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.anisotropy = 16; // 高质量过滤
    texture.generateMipmaps = true;
    texture.minFilter = THREE.LinearMipmapLinearFilter;
    texture.magFilter = THREE.LinearFilter;
  });
  // 新建简单稳定地球材质
  const earthMaterial = new THREE.MeshPhongMaterial({
    map: earthDayTexture,
    bumpMap: earthBumpTexture,
    bumpScale: 0.1,
    shininess: 0,
  });
  // 海洋蓝色附加层
  const oceanMaterial = new THREE.ShaderMaterial({
    uniforms: {
      uDayTexture: { value: earthDayTexture },
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
          vUv = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
    fragmentShader: `
        uniform sampler2D uDayTexture;
        varying vec2 vUv;

        void main() {
          vec3 texColor = texture2D(uDayTexture, vUv).rgb;

          // 海洋判断: 蓝色多,绿红少
          float isOcean = step(0.5, texColor.b) * step(texColor.r, 0.3) * step(texColor.g, 0.4);

          // 海洋部分替换为深蓝
          vec3 oceanColor = vec3(0.1, 0.3, 0.7);
          vec3 finalColor = mix(texColor, oceanColor, isOcean);

          gl_FragColor = vec4(finalColor, 1.0);
        }
      `,
    transparent: false,
  });
  // 月球着色器材质(高分辨率)
  const moonGeometry = new THREE.SphereGeometry(0.8, 128, 128);
  // NASA 月球高分辨率纹理 - 带备用
  const moonTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/moon.jpg", undefined, undefined, () =>
    textureLoader.load("https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_poles_1k.jpg")
  );
  const moonBumpTexture = textureLoader.load("https://unpkg.com/three-globe@2.24.3/example/img/moon.jpg", undefined, undefined, () =>
    textureLoader.load("https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_poles_1k.jpg")
  );
  [moonTexture, moonBumpTexture].forEach((texture) => {
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.anisotropy = 16;
    texture.generateMipmaps = true;
    texture.minFilter = THREE.LinearMipmapLinearFilter;
    texture.magFilter = THREE.LinearFilter;
  });
  const moonMaterial = new THREE.ShaderMaterial({
    uniforms: {
      time: { value: 0.0 },
      sunDirection: { value: new THREE.Vector3(-1, 0.5, 1).normalize() },
      uMoonTexture: { value: moonTexture },
      uMoonBump: { value: moonBumpTexture },
    },
    vertexShader: `
        varying vec3 vNormal;
        varying vec3 vPosition;
        varying vec2 vUv;
        varying vec3 vViewPosition;

        void main() {
          vNormal = normalize(normalMatrix * normal);
          vPosition = position;
          vUv = uv;

          vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
          vViewPosition = -mvPosition.xyz;
          gl_Position = projectionMatrix * mvPosition;
        }
      `,
    fragmentShader: `
        uniform float time;
        uniform vec3 sunDirection;
        uniform sampler2D uMoonTexture;
        uniform sampler2D uMoonBump;
        varying vec3 vNormal;
        varying vec3 vPosition;
        varying vec2 vUv;
        varying vec3 vViewPosition;

        void main() {
          vec3 normal = normalize(vNormal);
          vec3 lightDir = normalize(sunDirection);

          // 使用NASA月球纹理
          vec3 moonColor = texture2D(uMoonTexture, vUv).rgb;

          // 凹凸贴图显示月球地形
          float bumpValue = texture2D(uMoonBump, vUv).r;
          vec3 bumpNormal = normal + (bumpValue - 0.5) * 0.3;
          bumpNormal = normalize(bumpNormal);

          // 阳光照明计算
          float lightIntensity = dot(bumpNormal, lightDir);
          float NdotL = max(0.0, lightIntensity);

          // 阴阳分界线
          float terminator = smoothstep(-0.1, 0.1, lightIntensity);

          // 月相
          float phaseAngle = dot(normalize(vPosition), lightDir);
          float phase = smoothstep(-0.4, 0.4, phaseAngle);

          // 月球真实反照率
          float albedo = 0.12; // 月球平均反照率

          // 最终照明
          float ambientLight = 0.02; // 太空环境光
          float diffuseLight = NdotL * albedo;

          // 调整月球颜色
          moonColor *= vec3(1.1, 1.0, 0.9);

          vec3 finalColor = moonColor * (ambientLight + diffuseLight * terminator * phase);

          // 地球反射光
          float earthShine = (1.0 - terminator) * 0.08;
          finalColor += moonColor * earthShine * vec3(0.3, 0.4, 0.6);

          // 伽马校正
          finalColor = pow(finalColor, vec3(1.0/2.2));

          gl_FragColor = vec4(finalColor, 1.0);
        }
      `,
  });
  // 创建地球和月球网格(使用海洋材质)
  const earth = new THREE.Mesh(earthGeometry, oceanMaterial);
  const moon = new THREE.Mesh(moonGeometry, moonMaterial);
  // 地球位置(右下)
  earth.position.set(5, -3, -15);
  // 月球位置(左上)
  moon.position.set(-6, 4, -12);
  // 星空背景
  const starsGeometry = new THREE.BufferGeometry();
  const starsMaterial = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 2,
    sizeAttenuation: false,
    transparent: true,
    opacity: 0.8,
  });
  const starsVertices = [];
  for (let i = 0; i < 2000; i++) {
    const x = (Math.random() - 0.5) * 2000;
    const y = (Math.random() - 0.5) * 2000;
    const z = (Math.random() - 0.5) * 2000;
    starsVertices.push(x, y, z);
  }
  starsGeometry.setAttribute("position", new THREE.Float32BufferAttribute(starsVertices, 3));
  const stars = new THREE.Points(starsGeometry, starsMaterial);
  // 地球大气光(辉光效果)
  const atmosphereGeometry = new THREE.SphereGeometry(3.1, 64, 64);
  const atmosphereMaterial = new THREE.ShaderMaterial({
    uniforms: {
      viewVector: { value: camera.position },
    },
    vertexShader: `
        uniform vec3 viewVector;
        varying float intensity;
        void main() {
          vec3 vNormal = normalize(normalMatrix * normal);
          vec3 vNormel = normalize(normalMatrix * viewVector);
          intensity = pow(0.6 - dot(vNormal, vNormel), 2.0);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
    fragmentShader: `
        varying float intensity;
        void main() {
          vec3 glow = vec3(0.3, 0.6, 1.0) * intensity;
          gl_FragColor = vec4(glow, intensity * 0.8);
        }
      `,
    side: THREE.BackSide,
    blending: THREE.AdditiveBlending,
    transparent: true,
  });
  const earthAtmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
  earthAtmosphere.position.copy(earth.position);
  // 更真实的光照
  const ambientLight = new THREE.AmbientLight(0x404040, 0.1);
  const sunLight = new THREE.DirectionalLight(0xffffff, 1.0);
  sunLight.position.set(-10, 5, 8);
  sunLight.castShadow = true;
  // 添加到场景
  scene.add(earth);
  scene.add(earthAtmosphere);
  scene.add(moon);
  scene.add(stars);
  scene.add(ambientLight);
  scene.add(sunLight);
  // 摄像机位置
  camera.position.z = 10;
  // 动画循环
  let planetTime = 0;
  function animatePlanets() {
    planetTime += 0.01;
    // 地球自转
    earth.rotation.y += 0.01;
    // 月球自转和公转
    moon.rotation.y += 0.02;
    moon.position.x = -6 + Math.cos(planetTime * 0.5) * 2;
    moon.position.y = 4 + Math.sin(planetTime * 0.5) * 1;
    // 更新着色器uniform
    moonMaterial.uniforms.time.value = planetTime;
    // 星空微旋转
    stars.rotation.y += 0.0005;
    stars.rotation.x += 0.0002;
    renderer.render(scene, camera);
    requestAnimationFrame(animatePlanets);
  }
  animatePlanets();
  // 窗口大小变化响应
  window.addEventListener("resize", () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });
</script>
</body>
</html>

效果如下

相关推荐
gis分享者2 天前
学习threejs,实现山谷奔跑效果
threejs·着色器·glsl·shadermaterial·unrealbloompass·山谷奔跑·simplex
twe77582583 天前
用3D动画揭示技术路线的多样性
科技·3d·制造·动画
ct9783 天前
ThreeJs材质、模型加载、核心API
webgl·材质·threejs
a1117765 天前
飞机躲避炸弹 网页游戏
前端·开源·html·threejs
a1117765 天前
3D赛车躲避游戏(html threeJS开源)
前端·游戏·3d·开源·html·threejs
a1117766 天前
水体渲染系统(html开源)
前端·开源·threejs·水体渲染
LqKKsNUdXlA7 天前
多通道卷积神经网络 变压器 故障诊断 MATLAB (附赠变压器振动信号数据集) 关键词
动画
twe775825812 天前
参数调控与3D动画的互动魅力
科技·3d·制造·动画
twe775825815 天前
用3D动画解密3D IC封装中的微观世界
科技·3d·制造·动画
_风华ts20 天前
创建并使用AimOffset
ue5·动画·虚幻·虚幻引擎·aimoffset