一、前言
最近老米又整了个大活,上线了三月传说的剧情。
不得不说原神的美工这一块真的没话说,身为一个前端切图仔,马上被他的 web 特效吸引。刚好最近在学习 Three.js,想着能否通过复刻一个新活动场景来练练手。于是,我开始了这场 Three.js 的奇妙之旅。
最终效果展示
GitHub 地址:https://github.com/qirong77/genshin-impact-moon
在线访问地址:https://qirong77.github.io/genshin-impact-moon/
个人精力有限,只做了一个页面,历时五天。差不多还原了这个页面的 90% 的功能。
二、场景分析
这个页面其实不太难,在动手之前,我先仔细观察了原神空月之歌场景,发现主要有以下几个部分:
- 主体背景:包括渐变的主体背景和左上角旋落的流星、场景中会闪现随机分布的星星🌟。
- 星环:由多个同心圆环组成,具有动态旋转效果。
- 坐标轴:场景中的坐标轴装饰。
2.1 主体背景实现
主体背景是一个静态的图片,要实现的话直接导入就可以。
我直接从原神的活动页面抓取了背景图片资源。通过分析网络请求,找到了背景图片的链接,并将其下载到本地项目中。在 Three.js 中,使用 THREE.TextureLoader
加载背景图片,并将其设置为场景的背景。
- 勾选请求类型 image
- 右侧下载
可以看到除了背景也有很多其他的资源,我们全部 copy 下来放到我们的项目中。使用 Threejs 进行加载:
js
const textureLoader = new THREE.TextureLoader();
const backgroundTexture = textureLoader.load('path/to/background.jpg');
scene.background = backgroundTexture;
使用 threejs 加载后,为了符合深空的背景,我整体使用紫色和暗色调。在使用 gui 调整整体的透明度,旋转、位置等参数后,得到了初步的效果。
背景中还有一些散落的星星,我在抓包的时候看到老米使用的背景星星好像是使用贴图是不断放大缩小实现的,我觉得这样还是太敷衍了。所以我决定使用 Threejs 中的网格自己实现一个。
首先,创建了一个包含大量点的 THREE.Points
网格每里面大量的点的位置是随机的。
js
// 创建星星点点
function createStarField() {
const vertices = [];
for (let i = 0; i < 10000; i++) {
const x = (Math.random() - 0.5) * 2000;
const y = (Math.random() - 0.5) * 2000;
const z = (Math.random() - 0.5) * 2000;
vertices.push(x, y, z);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 1,
transparent: true
});
return new THREE.Points(geometry, material);
}
然后对于每个点我们再使用 shader 进行优化,为了让星星具有闪烁效果,使用了自定义的 ShaderMaterial
,通过在顶点着色器和片段着色器中添加时间变量,控制星星的透明度和亮度变化。以下是部分着色器代码:
c
// 顶点着色器
varying vec3 vPosition;
void main() {
vPosition = position;
gl_PointSize = 1.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// 片段着色器
uniform float time;
varying vec3 vPosition;
void main() {
float brightness = sin(time + length(vPosition)) * 0.5 + 0.5;
gl_FragColor = vec4(vec3(brightness), brightness);
}
通过不断调整着色器中的参数,最终实现了星星的闪烁效果,使其更加贴近原神中的视觉表现。
2.2 星环实现
星环是整个场景中最复杂的部分之一。为了实现星环的动态旋转效果,需要创建了多个同心圆环,并为每个圆环添加了不同的旋转速度和方向。每个圆环由一系列点组成,这些点同样使用了自定义的着色器来实现发光和闪烁效果。
js
export function createCircle(
imagePath = CirclePath,
circleName = "circlename",
defaultValue = {
circleSize: 3.5,
rotationSpeed: 0.5, // 默认旋转速度
opacity: 0.5,
}
) {
// 添加图片到圆环中心
const textureLoader = new THREE.TextureLoader();
const circleTexture = textureLoader.load(imagePath);
const circleGeometry = new THREE.PlaneGeometry(1, 1); // 平面大小根据图片调整
const circleMaterial = new THREE.MeshBasicMaterial({
map: circleTexture,
transparent: true,
side: THREE.DoubleSide,
alphaMap: alphaTexture,
opacity: defaultValue.opacity,
alphaTest: 0.1,
});
const circleMesh = new THREE.Mesh(circleGeometry, circleMaterial);
circleMesh.scale.set(Number(defaultValue.circleSize), Number(defaultValue.circleSize), 1);
circleMesh.position.z = 0.1; // 稍微调整z轴避免与星星重叠
const folder = gui.addFolder(circleName);
folder.close(); // 默认收起面板
const controls = {
...defaultValue,
};
// 新增图片大小控制
folder.add(controls, "circleSize", 1, 10).onChange((value) => {
circleMesh.scale.set(Number(value), Number(value), 1);
});
// 添加旋转速度控制
folder.add(controls, "rotationSpeed", -0.1, 0.1).name("旋转速度");
function animate() {
// return
requestAnimationFrame(animate);
// 更新圆环旋转
circleMesh.rotation.z += controls.rotationSpeed * 0.01;
}
animate();
return circleMesh;
}
如果直接加载图片的话会显得不那么真实,看到老米的星环是有一种模糊的质感,于是通过分析发现老米应该是用了贴图纹理,找到资源中相关的图片,将星环加上质感。
js
// 加载星环纹理
const starRingTexture = textureLoader.load('path/to/star-ring-texture.png');
starRingTexture.wrapS = THREE.RepeatWrapping;
starRingTexture.wrapT = THREE.RepeatWrapping;
2.3 、坐标轴
图片场景还有一个坐标轴,我们直接使用 Point 将点随机分布在坐标轴上就行。
js
export function createAxisStars() {
const geometry = new THREE.BufferGeometry();
const vertices = [];
for (let i = 0; i < 100; i++) {
const x = -500;
const y = (Math.random() - 0.5) * 1000;
const z = (Math.random() - 0.5) * 1000;
vertices.push(x, y, z);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 2
});
return new THREE.Points(geometry, material);
}
三、最中相机视角确定
目前我们已经完成了需要使用到的所有组件,但是目前我们还是在一个三维的视角中。
现在需要完成最终相机的位置确定,就是用户最终看到的画面。 为了方便,我们将背景的坐标固定在 xy 平面上,将中间倾斜的主体放到一个 group中进行整体旋转
js
const galaxyGroup = new THREE.Group();
galaxyGroup.add(ringItem1);
galaxyGroup.add(ringItem2);
galaxyGroup.add(startRing1);
galaxyGroup.add(startRing2);
galaxyGroup.add(startRing3);
galaxyGroup.add(startRing4);
galaxyGroup.add(circle1);
galaxyGroup.add(circle2);
galaxyGroup.add(circle3);
galaxyGroup.add(circle4);
galaxyGroup.add(circle5);
galaxyGroup.add(axisStar);
galaxyGroup.rotation.x = -0.8;
galaxyGroup.rotation.y = -0.21;
galaxyGroup.rotation.z = -0.18;
然再调整相关的参数(炼丹),就大差不差了:
四、整体优化
最后,再对整个场景进行了优化,以确保在不同设备上都能流畅运行。
- 性能优化:通过减少星星和星环的数量、降低着色器的复杂度等方式,在保证视觉效果的同时提高渲染性能。
- 响应式设计:添加了窗口大小变化的监听器,使场景能够自适应不同的屏幕尺寸。
- 参数调整 :使用
dat.GUI
添加了调试界面,方便在运行时调整各个元素的参数,如星星的大小、星环的旋转速度等。
js
const gui = new dat.GUI();
gui.add(material.uniforms.size, 'value', 0.1, 5).name('星星大小');
gui.add(material.uniforms.glowIntensity, 'value', 0, 2).name('发光强度');
通过不断调试和优化,最终实现了与原神空月之歌场景相似的视觉效果。