夜色渐浓,众星拱月 - 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/

相关推荐
好_快30 分钟前
Lodash源码阅读-baseMatchesProperty
前端·javascript·源码阅读
好_快31 分钟前
Lodash源码阅读-hasPath
前端·javascript·源码阅读
好_快34 分钟前
Lodash源码阅读-hasIn
前端·javascript·源码阅读
Jasmin Tin Wei37 分钟前
蓝桥杯 web 展开你的扇子(css3)
前端·css·css3
好_快38 分钟前
Lodash源码阅读-basePropertyDeep
前端·javascript·源码阅读
vvilkim4 小时前
深入理解 TypeScript 中的 implements 和 extends:区别与应用场景
前端·javascript·typescript
GISer_Jing4 小时前
前端算法实战:大小堆原理与应用详解(React中优先队列实现|求前K个最大数/高频元素)
前端·算法·react.js
写代码的小王吧6 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
小小小小宇6 小时前
CSS 渐变色
前端
snow@li7 小时前
前端:开源软件镜像站 / 清华大学开源软件镜像站 / 阿里云 / 网易 / 搜狐
前端·开源软件镜像站