Frontend Development 10 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Recreating Genshin Impact Moon Scene with Three.js – A Step‑by‑Step Tutorial

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.

Three.jsWebGLShader3D graphicsdat.GUIFrontend TutorialGenshin Impact
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.