Skip to content

Instantly share code, notes, and snippets.

@JMHeartley
Created January 18, 2026 23:24
Show Gist options
  • Select an option

  • Save JMHeartley/88efefbb1aef817f3f65f284cfca55bb to your computer and use it in GitHub Desktop.

Select an option

Save JMHeartley/88efefbb1aef817f3f65f284cfca55bb to your computer and use it in GitHub Desktop.
Interactive 3D Sphere with Shaders and Swirling Ribbons in Three.js
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
overflow: hidden;
}
#container {
width: 100vw;
height: 100vh;
background: radial-gradient(circle at center, #0d0d1a 0%, #000 100%);
position: fixed;
top: 0;
left: 0;
}
canvas {
position: fixed;
top: 0;
left: 0;
}
</style>
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.set(0, 0, 8);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.getElementById('container').appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
const sphereShader = {
vertexShader: `
varying vec3 vNormal;
varying vec3 vPosition;
uniform float time;
void main() {
vNormal = normalize(normalMatrix * normal);
float pulsate = sin(time + length(position) * 4.0) * 0.1;
vec3 newPos = position + vNormal * pulsate;
vPosition = newPos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
`,
fragmentShader: `
varying vec3 vNormal;
varying vec3 vPosition;
uniform float time;
void main() {
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.7));
float diffuse = dot(vNormal, lightDir) * 0.5 + 0.5;
vec3 baseColor = vec3(
0.5 + 0.5 * sin(time + vPosition.x * 2.0),
0.5 + 0.5 * cos(time + vPosition.y * 2.0),
0.7 + 0.3 * sin(time + vPosition.z * 2.0)
);
float fresnel = pow(1.0 - dot(vNormal, normalize(cameraPosition - vPosition)), 2.0);
vec3 rimColor = vec3(1.0, 0.8, 1.0) * fresnel;
gl_FragColor = vec4(baseColor * diffuse + rimColor, 1.0);
}
`
};
const sphereGeometry = new THREE.SphereGeometry(1.5, 64, 64);
const sphereMaterial = new THREE.ShaderMaterial({
uniforms: { time: { value: 0 } },
vertexShader: sphereShader.vertexShader,
fragmentShader: sphereShader.fragmentShader,
side: THREE.DoubleSide,
transparent: false
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);
const ribbonShader = {
vertexShader: `
varying vec2 vUv;
uniform float time;
void main() {
vUv = uv;
vec3 pos = position;
float twist = sin(pos.y * 4.0 + time) * 0.5;
pos.x += twist;
pos.z += cos(pos.y * 4.0 + time) * 0.5;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform float time;
void main() {
vec3 colorStart = vec3(0.2, 0.4, 1.0);
vec3 colorEnd = vec3(0.8, 0.2, 1.0);
float gradient = smoothstep(0.0, 1.0, vUv.y);
vec3 color = mix(colorStart, colorEnd, gradient);
float shimmer = sin(vUv.x * 40.0 + time * 5.0) * 0.2 + 0.8;
gl_FragColor = vec4(color * shimmer, 0.7);
}
`
};
const ribbonCount = 8;
const ribbons = [];
for (let i = 0; i < ribbonCount; i++) {
const angle = (i / ribbonCount) * Math.PI * 2;
const ribbonMaterial = new THREE.ShaderMaterial({
uniforms: { time: { value: 0 } },
vertexShader: ribbonShader.vertexShader,
fragmentShader: ribbonShader.fragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide
});
const ribbonGeometry = new THREE.PlaneGeometry(0.1, 6, 1, 50);
const ribbon = new THREE.Mesh(ribbonGeometry, ribbonMaterial);
ribbon.position.set(Math.cos(angle) * 3, 0, Math.sin(angle) * 3);
ribbon.lookAt(0, 0, 0);
ribbons.push(ribbon);
scene.add(ribbon);
}
const starCount = 5000;
const starGeometry = new THREE.BufferGeometry();
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
starPositions[i * 3] = (Math.random() - 0.5) * 200;
starPositions[i * 3 + 1] = (Math.random() - 0.5) * 200;
starPositions[i * 3 + 2] = (Math.random() - 0.5) * 200;
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
sizeAttenuation: true,
transparent: true,
opacity: 0.8
});
const starField = new THREE.Points(starGeometry, starMaterial);
scene.add(starField);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 1.5, 50);
pointLight.position.set(5, 10, 7.5);
scene.add(pointLight);
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
let time = clock.getElapsedTime();
controls.update();
sphereMaterial.uniforms.time.value = time;
ribbons.forEach(ribbon => {
ribbon.material.uniforms.time.value = time;
});
sphere.rotation.y += 0.001;
ribbons.forEach((ribbon, i) => {
ribbon.rotation.y += 0.005;
});
starField.rotation.y += 0.0005;
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>

Interactive 3D Sphere with Shaders and Swirling Ribbons in Three.js

Interactive 3D sphere with GLSL shaders, swirling ribbons, and a starfield. Built with Three.js, this responsive scene offers smooth interactivity for web developers and 3D enthusiasts.

A Pen by Justin Heartley on CodePen.

License.

// Interaction Instructions:
// 1. Click and drag to rotate the scene.
// 2. Use your mouse/trackpad scroll to zoom in and out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment