Recreating Genshin Impact Moon Scene with Three.js – A Step‑by‑Step Tutorial
This article walks through recreating Genshin Impact’s Moon scene using Three.js, covering asset extraction, background loading, star field generation with custom shaders, concentric ring creation, axis stars, camera grouping, and performance optimizations, complete with full source code and live demo links.
Introduction
The author, a front‑end developer, was inspired by the visual effects of Genshin Impact’s "Moon Song" event and decided to replicate the scene using Three.js as a learning project.
Final Result
GitHub repository: https://github.com/qirong77/genshin-impact-moon
Live demo: https://qirong77.github.io/genshin-impact-moon/
Scene Analysis
Background : Gradient background with a falling meteor and randomly distributed stars.
Star Ring : Multiple concentric circles with dynamic rotation.
Axis : Decorative coordinate axes.
Background Implementation
The static background image is loaded with THREE.TextureLoader and set as the scene background.
const textureLoader = new THREE.TextureLoader();
const backgroundTexture = textureLoader.load('path/to/background.jpg');
scene.background = backgroundTexture;Additional resources were copied into the project and loaded similarly.
Star Field Implementation
A dense point cloud is created using THREE.Points . Random positions are generated for 10,000 points, stored in a THREE.BufferGeometry , and rendered with a custom THREE.PointsMaterial . A ShaderMaterial adds a flickering effect.
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);
}The shader code controls brightness over time:
// Vertex shader
varying vec3 vPosition;
void main() {
vPosition = position;
gl_PointSize = 1.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// Fragment shader
uniform float time;
varying vec3 vPosition;
void main() {
float brightness = sin(time + length(vPosition)) * 0.5 + 0.5;
gl_FragColor = vec4(vec3(brightness), brightness);
}Star Ring Implementation
Concentric rings are built with THREE.PlaneGeometry textured with a ring image. GUI controls (dat.GUI) allow adjusting size and rotation speed.
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,
opacity: defaultValue.opacity,
alphaTest: 0.1,
});
const circleMesh = new THREE.Mesh(circleGeometry, circleMaterial);
circleMesh.scale.set(Number(defaultValue.circleSize), Number(defaultValue.circleSize), 1);
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() {
requestAnimationFrame(animate);
circleMesh.rotation.z += controls.rotationSpeed * 0.01;
}
animate();
return circleMesh;
}The ring texture is loaded and set to repeat:
const starRingTexture = textureLoader.load('path/to/star-ring-texture.png');
starRingTexture.wrapS = THREE.RepeatWrapping;
starRingTexture.wrapT = THREE.RepeatWrapping;Axis Stars
Simple points are placed along a fixed X coordinate to form an axis.
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);
}Camera and Group Setup
All components are added to a THREE.Group which is rotated to achieve the desired viewing angle.
const galaxyGroup = new THREE.Group();
galaxyGroup.add(ringItem1);
galaxyGroup.add(ringItem2);
galaxyGroup.add(startRing1);
// ... other items ...
galaxyGroup.rotation.x = -0.8;
galaxyGroup.rotation.y = -0.21;
galaxyGroup.rotation.z = -0.18;Overall Optimization
Performance : Reduced star and ring counts and simplified shaders.
Responsive Design : Added window‑resize listener for adaptive layout.
Parameter Adjustment : Integrated dat.GUI to tweak star size, glow intensity, ring speed, etc.
const gui = new dat.GUI();
gui.add(material.uniforms.size, 'value', 0.1, 5).name('星星大小');
gui.add(material.uniforms.glowIntensity, 'value', 0, 2).name('发光强度');After iterative testing and tuning, the final scene closely resembles the original Genshin Impact Moon Song visual.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.