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.
| <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 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.
| // Interaction Instructions: | |
| // 1. Click and drag to rotate the scene. | |
| // 2. Use your mouse/trackpad scroll to zoom in and out. |