Frontend Development 23 min read

Creating a Pepyaka Shader Effect with Three.js: Sphere, Noise, Particle System, and Text Overlay

This tutorial walks through reproducing the Pepyaka shader effect using Three.js, covering the creation of a central sphere, vertex displacement with GLSL noise, color mapping, spherical particle systems, background firefly particles, animated text, and optional top‑light effects, all with complete source code.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating a Pepyaka Shader Effect with Three.js: Sphere, Noise, Particle System, and Text Overlay

Preface

The author revisits a popular Three.js shader particle system tutorial that previously gained high engagement on Juejin, aiming to recreate the striking Pepyaka effect for front‑end developers.

Central Sphere

Starting from a simple wireframe sphere, the basic scene, camera, renderer, and controls are set up. A minimal vertex and fragment shader render a white sphere, and a uniform uTime is introduced for animation.

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
let w = window.innerWidth;
let h = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, w / h, 0.01, 1000);
camera.position.set(0, 0, 4);
camera.lookAt(new THREE.Vector3());
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
renderer.setClearColor(0x0a0a0f, 1);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const vertexShader = `
  uniform float uTime;
  void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;
const fragmentShader = `
  void main() {
    gl_FragColor = vec4(vec3(1.0), 1.0);
  }
`;
const sphereMaterial = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: { uTime: { value: 0 } }
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
const clock = new THREE.Clock();
function render() {
  const time = clock.getElapsedTime();
  sphereMaterial.uniforms.uTime.value = time;
  sphere.rotation.y = time;
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
render();

Vertex Displacement with Noise

GLSL Simplex 4‑D noise is imported and applied to each vertex. The noise value is multiplied by the vertex normal to offset the position, creating an organic, pulsating shape. The same noise drives the hue in HSV color space for vivid coloring.

// vertex shader
uniform float uTime;
varying vec3 vNormal;
varying vec3 vColor;
float snoise(vec4 v) { /* implementation omitted for brevity */ }
void main() {
  vNormal = normal;
  float noise = snoise(vec4(position, 0.0));
  vColor = hsv2rgb(vec3(noise, 1.0, 1.0));
  vec3 newPos = position + 0.8 * normal * noise;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
// fragment shader
varying vec3 vColor;
void main() {
  gl_FragColor = vec4(vColor, 1.0);
}

Spherical Particle System

A second BufferGeometry generates 4000 points uniformly distributed on a sphere using a Fibonacci‑spiral formula. Each particle receives a size attribute that influences its point size and later its animation speed.

const particleGeometry = new THREE.BufferGeometry();
const N = 4000;
const positions = new Float32Array(N * 3);
const inc = Math.PI * (3 - Math.sqrt(5));
const off = 2 / N;
const radius = 2;
for (let i = 0; i < N; i++) {
  const y = i * off - 1 + off / 2;
  const r = Math.sqrt(1 - y * y);
  const phi = i * inc;
  positions[3 * i] = radius * Math.cos(phi) * r;
  positions[3 * i + 1] = radius * y;
  positions[3 * i + 2] = radius * Math.sin(phi) * r;
}
particleGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));

The particle material uses a simple shader that sets point size based on distance and renders semi‑transparent white points.

const particleVertex = `
  uniform float uTime;
  void main() {
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = 6.0 / -mvPosition.z;
    gl_Position = projectionMatrix * mvPosition;
  }
`;
const particleFragment = `
  void main() {
    gl_FragColor = vec4(vec3(1.0), 0.6);
  }
`;
const particleMaterial = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: particleVertex,
  fragmentShader: particleFragment,
  transparent: true,
  blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);

Particle Vertical Oscillation

By adding a sinusoidal offset to the y coordinate inside the particle vertex shader, particles move up and down in wave‑like patterns, enhancing the dynamic feel.

uniform float uTime;
void main() {
  vec3 newPos = position;
  newPos.y += 0.1 * sin(newPos.y * 6.0 + uTime);
  vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
  gl_PointSize = 6.0 / -mvPosition.z;
  gl_Position = projectionMatrix * mvPosition;
}

Background Random Particles (Fireflies)

A third particle system creates 300 firefly‑like points scattered in a larger radius. Each point’s size drives both its visual size and animation speed. The fragment shader computes opacity based on distance from the point center, yielding a soft glowing disc.

const firefliesGeometry = new THREE.BufferGeometry();
const firefliesCount = 300;
const positions1 = new Float32Array(firefliesCount * 3);
const sizes = new Float32Array(firefliesCount);
for (let i = 0; i < firefliesCount; i++) {
  const r = Math.random() * 5 + 5;
  positions1[i * 3] = (Math.random() - 0.5) * r;
  positions1[i * 3 + 1] = (Math.random() - 0.5) * r;
  positions1[i * 3 + 2] = (Math.random() - 0.5) * r;
  sizes[i] = Math.random() + 0.4;
}
firefliesGeometry.setAttribute("position", new THREE.BufferAttribute(positions1, 3));
firefliesGeometry.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1));
const firefliesVertexShader = `
  uniform float uTime;
  attribute float aSize;
  void main() {
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = 70.0 * aSize / -mvPosition.z;
    gl_Position = projectionMatrix * mvPosition;
  }
`;
const firefliesFragmentShader = `
  void main() {
    float d = length(gl_PointCoord - vec2(0.5));
    float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
    gl_FragColor = vec4(vec3(1.0), strength);
  }
`;
const firefliesMaterial = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: firefliesVertexShader,
  fragmentShader: firefliesFragmentShader,
  transparent: true,
  blending: THREE.AdditiveBlending,
  depthWrite: false
});
const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial);
scene.add(fireflies);

Displaying Text

A plane geometry loads a PNG with transparent background containing the desired text. The shader samples the texture via UV coordinates, and a small sinusoidal deformation adds a subtle floating motion.

const textGeometry = new THREE.PlaneGeometry(2, 1, 100, 100);
const textVertex = `
  uniform float uTime;
  varying vec2 vUv;
  void main() {
    vUv = uv;
    vec3 newPos = position;
    newPos.y += 0.06 * sin(newPos.x + uTime);
    newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
  }
`;
const textFragment = `
  uniform sampler2D uTexture;
  varying vec2 vUv;
  void main() {
    vec4 color = texture2D(uTexture, vUv);
    gl_FragColor = color;
  }
`;
const textMaterial = new THREE.ShaderMaterial({
  vertexShader: textVertex,
  fragmentShader: textFragment,
  uniforms: {
    uTime: { value: 0 },
    uTexture: { value: new THREE.TextureLoader().load("https://i.postimg.cc/nrSTmrZk/text.png") }
  },
  transparent: true
});
const text = new THREE.Mesh(textGeometry, textMaterial);
text.position.z = 1.7;
scene.add(text);

Top Light (Optional)

The original effect includes a top‑light beam; the author notes it is omitted for brevity but can be added later with an additional shader pass.

Conclusion

The tutorial reproduces the Pepyaka shader effect, covering sphere deformation, noise‑driven color, multiple particle systems, animated text, and background fireflies. While the result is a simplified version of the original, it provides a solid foundation for further experimentation and optimization.

Full source code is available on CodePen: https://codepen.io/GuLiu/pen/LYvazVG .

graphicsJavaScriptTutorialThree.jsWebGLShaderParticle System
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.