夜色渐浓,众星拱月 - Threejs复刻原神绝美空月之歌场景(二)

一、前言

之前完成了 Three.js 复刻的一版空月之歌:juejin.cn/post/748185...

最近空月之歌的活动页面又更新了,新增了一些特效和交互。正好最近在学习 Three.js,便想着通过复刻新活动场景来练练手。于是,开启了这场 Three.js 的奇妙之旅。

最终效果展示

GitHub 地址:https://github.com/qirong77/genshin-impact-moon 喜欢的话点个🌟 谢谢~

在线访问地址:https://qirong77.github.io/genshin-impact-moon/

这次开发大约花了 3 天时间(再多就没法干了,老米不给我发工资)

部分 UI 和样式可能没有完全还原,欢迎感兴趣的同学 PR 支持~目前仍在持续优化中。

二、场景分析

本次更新主要新增了以下功能:

  1. 丝滑的来回切换动画:如上图所示。
  2. 动态图标:11 个地方势力的图标在圆环上动态移动。
  3. 地方势力详情展示:包括图案主体、说明文案,以及右侧 TAB 的切换功能。

三、实现思路

3.1 切换动画实现

初步实现动画

为了更直观地理解动画的实现,我们从动画的初始状态和最终状态入手进行推测。

动画开始时,轮盘是倾斜的;动画结束时,轮盘垂直于用户视角。

根据 3D 视角原理,这可以通过以下两种方式实现:

  1. 移动相机,保持轮盘不动。
  2. 移动轮盘,保持相机不动。

通过观察官网动画的背景未发生倾斜,可以推测是轮盘发生了移动。

假设轮盘的初始旋转角度为 rotation = [X, Y, Z],目标状态为 [0, 0, Z](因为最终轮盘是正对我们的,所以 xy 的坐标为 0)

我们需要一个平滑的变化函数来实现这一过渡。类似于 CSS 动画中的线性过渡或平滑过渡。

我们可以使用第三方库 gsap,其内置了多种过渡函数,能够轻松实现这一效果。

js 复制代码
function galaxyAnimation() {
  // 遍历所有子元素并设置透明度动画
  galaxyGroup.children.forEach((child) => {
    gsap.to(galaxyGroup.rotation, {
      x: 0,
      y: 0,
      z: 0,
      duration: 2,
      ease,
    });
    gsap.to(galaxyGroup.position, {
      x: 0,
      y: 0,
      z: -2,
      duration: 2,
      ease,
    });
  }

提升动画的视觉效果

目前的动画虽然功能完整,但缺乏视觉冲击力。通过进一步分析,原动画不仅包含旋转,还伴随着轮盘的放大(即 Z 轴方向的拉近)。这利用了近大远小的视觉原理。

因此,我们可以在旋转动画的基础上,叠加一个拉近拉远的动画,从而实现更具冲击力的场景切换效果。

!!注意和上图的效果进行对比。

js 复制代码
function cameraAnimation() {
    const { x, y, z } = camera.position;
    gsap.to(camera.position, {
        x,
        y,
        z: z - 2,
        duration: 1,
        ease,
        onComplete: () => {
            gsap.to(camera.position, {
                x,
                y,
                z,
                duration: 1,
                ease,
            });
        },
    });
}

3.2 地方势力的图标实现

分析

地方势力就是刚开始的页面中的那些图标,这些图标围绕一个圆盘移动。通常情况下,只需更新每个物体的 rotation 即可实现。

但如果使用平面展示每个物体 ,在旋转时会导致视角偏移。 如下图所示

为确保展示效果正确,应通过更新物体的位置而非整体旋转来实现移动。所以这里就会涉及到一些数学的计算,不过也不麻烦,可以简单看下面的代码:

js 复制代码
function animate() {
    // 增加旋转角度
    rotationAngle += 0.001;

    // 更新每个mesh的位置,而不是旋转整个group
    initialPositions.forEach((item) => {
        // 使用旋转矩阵计算新的位置
        const cos = Math.cos(rotationAngle);
        const sin = Math.sin(rotationAngle);

        // 应用旋转变换到初始位置
        const newX = item.initialX * cos - item.initialY * sin;
        const newY = item.initialX * sin + item.initialY * cos;

        // 更新位置,保持z和朝向不变
        item.mesh.position.set(newX, newY, item.mesh.position.z);
    });

    // 使用requestAnimationFrame实现连续动画
    requestAnimationFrame(animate);
}

光线投射器和高亮效果实现

当鼠标悬停在某个物体上时,会触发高亮效果(颜色加深、体积变大)。如下面所示:

实现步骤如下:

  1. 确定鼠标悬停的物体。
  2. 为该物体添加动画。

因为在 Three.js 场景中,无法直接使用类似 DOM 的事件监听。我们可以通过 raycaster 工具检测鼠标位置与物体的交互。

"Raycaster"(光线投射器 )基于光线投射原理,从一个起点沿特定方向(简单理解为用户的视角)发射虚拟光线 ,检测光线与场景中物体的相交情况。

图中橙色箭头类似光线投射路径,穿过不同几何物体,形象展示光线与物体的交互 ,体现 Raycaster 检测光线与物体相交的过程。

具体步骤如下:

  1. 创建 raycaster 实例。
  2. 将鼠标屏幕坐标转换为归一化设备坐标(NDC)。
  3. 使用 raycaster.setFromCamera 方法生成射线。
  4. 使用 raycaster.intersectObjects 检测射线与场景中物体的交点。
  5. 如果检测到交点,为相应物体添加高亮效果。

代码示例:

js 复制代码
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

window.addEventListener('mousemove', (event) => {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

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

    if (intersects.length > 0) {
        const hoveredObject = intersects[0].object;
        hoveredObject.material.emissive.setHex(0xff0000); // 高亮效果
    }
});

3.3 地方势力详情展示

扫描线动画实现

扫描线动画通过周期性高亮图片的局部区域,模拟灯光扫过的效果。

假设扫描线初始位置为 [0, 0.1] (图片从左到右的 10% 位置)。扫描线对应的像素会被高亮,可通过以下两种方式实现:

  1. 提高透明度。
  2. 增加颜色饱和度和亮度(通过同时提高 RGB 通道值,使颜色更接近白色)。

一般来说第二种方式更合理,因为透明度调整范围有限。

scss 复制代码
// 获取当前扫描位置
float scanPos = mod(u_time * u_scanSpeed, 1.0);

// 创建柔和的扫描线效果
float scanStart = scanPos;
float scanEnd = scanPos + u_scanWidth;

// 使用smoothstep创建平滑过渡的扫描线
float scanLine = smoothstep(scanStart - u_scanSmoothness, scanStart + u_scanSmoothness, v_uv.x) - 
                 smoothstep(scanEnd - u_scanSmoothness, scanEnd + u_scanSmoothness, v_uv.x);

最终实现效果

星星动效实现

下面是官方实现的截图:

星星动效就是在图标中添加星星移动效果。目前我能想到的还是通过 Shader 实现。

Shader 实现思路
  1. 确定星星的数量,满足条件的 UV 坐标作为星星圆心。
  2. 根据圆心生成随机方向 d 和速度 v,计算星星的当前坐标。
  3. 根据坐标和半径计算星星的辐射范围,并通过离圆心的距离确定发光强度。
  4. 为实现闪烁效果,可叠加一个闪烁函数,最终得出亮度值。
js 复制代码
    // 星星函数,根据位置和时间生成星星
    float star(vec2 uv, float index) {
        // 为每个星星生成一个随机位置
        float randomX = random(vec2(index, 0.5));
        float randomY = random(vec2(index, 0.8));
        
        // 添加基于时间的移动
        float moveX = sin(u_time * 0.1 * random(vec2(index, 0.3))) * u_starMovementSpeed;
        float moveY = cos(u_time * 0.1 * random(vec2(index, 0.7))) * u_starMovementSpeed;
        
        // 星星位置
        vec2 starPos = vec2(randomX + moveX, randomY + moveY);
        
        // 计算当前像素到星星中心的距离
        float dist = distance(uv, starPos);
        
        // 星星闪烁效果
        float twinkle = sin(u_time * u_twinkleSpeed * random(vec2(index, 0.9))) * 0.5 + 0.5;
        
        // 如果距离小于星星大小,则显示星星
        return smoothstep(u_starSize, 0.0, dist) * u_starBrightness * twinkle;
    }

最终效果如下:

null

虽然实现了需求 ,但增加星星数量后会导致性能问题,导致我的 M1Pro 直接卡死,应该需优化计算逻辑。不过目前我没想到优化的方式,就不把这些星星放到项目中了。

切换动画实现

如果让你实现下面的一个切换的 TAB,那么你需要创建几个实例呢?

第一想法是 11 个排成一列,然后不断地左右移动切换。

后面做的时候发现只要创建 1 个就行了,可以看到动图的前后切换中间有短暂的黑屏。我们只需要在切换下一个之前,修改对应的纹理图案就行。

STEP1:创建TAB栏和下面的描述(略)
STEP2:实现切换的动画

切换的动画有两个步骤:

  1. 将卡片移动到左后方(过渡动画)
  2. 将卡片移动到右后方(立刻执行, 此时有短暂的时间屏幕不会展示图片
  3. 将卡片移动到正前方(过渡动画)

下面是一个消失动画的举例:

js 复制代码
// 消失动画
const disappear = (imagePagh: string) => {
    NodeKraiState.isAnimation = true;
    NodeKraiState.isFirstShow = false;
    gsap.to(group.position, {
        x: -2,
        z: -2,
        duration: 0.8,
        ease: "power2.inOut",
    });
    gsap.to(mainImageMesh.material.uniforms.u_opacity, {
        value: 0,
        duration: 0.8,
        ease: "power2.inOut",
        onComplete: () => {
            mainImageMesh.material.uniforms.u_texture.value = new THREE.TextureLoader().load(imagePagh);
            appear();
        },
    });
};

最终效果如下:

四、补充

制作一个优秀的交互网站需要大量精力,尤其是细节的打磨。

例如当我们进行转场动画的时候,轮盘会突然加速然后平滑过渡到原来的速度来提升观感,篇幅有限就不追溯了

此外,调试参数和贴图优化也需要大量时间,对缺乏美术基础的前端来说是个挑战。

最后,欢迎访问项目地址并提出建议~ ,后面应该还会继续优化~

GitHub 地址:https://github.com/qirong77/genshin-impact-moon 喜欢的点个🌟吧 谢谢~

在线访问地址:https://qirong77.github.io/genshin-impact-moon/

相关推荐
Pedantic16 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘17 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆17 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师18 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆18 小时前
VSCode自动格式化三要素
前端
爱勇宝19 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen19 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user205855615181321 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode21 小时前
Redis 在生产项目的使用
前端·后端