| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Crystal Formation</title> |
| <style> |
| body { margin: 0; overflow: hidden; background: #000; } |
| canvas { display: block; } |
| </style> |
| </head> |
| <body> |
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" |
| } |
| } |
| </script> |
| |
| <script type="module"> |
| import * as THREE from 'three'; |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; |
| import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; |
| |
| |
| const scene = new THREE.Scene(); |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
| camera.position.set(0, 5, 15); |
| |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); |
| renderer.shadowMap.enabled = true; |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
| document.body.appendChild(renderer.domElement); |
| |
| |
| const composer = new EffectComposer(renderer); |
| const renderPass = new RenderPass(scene, camera); |
| composer.addPass(renderPass); |
| |
| const bloomPass = new UnrealBloomPass( |
| new THREE.Vector2(window.innerWidth, window.innerHeight), |
| 0.4, 0.4, 0.85 |
| ); |
| composer.addPass(bloomPass); |
| |
| |
| const chromaticAberrationShader = { |
| uniforms: { |
| tDiffuse: { value: null }, |
| uIntensity: { value: 0.003 } |
| }, |
| vertexShader: ` |
| varying vec2 vUv; |
| void main() { |
| vUv = uv; |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| } |
| `, |
| fragmentShader: ` |
| uniform sampler2D tDiffuse; |
| uniform float uIntensity; |
| varying vec2 vUv; |
| |
| void main() { |
| vec2 center = vUv - 0.5; |
| float dist = length(center); |
| vec2 direction = normalize(center); |
| |
| float aberration = uIntensity * dist; |
| |
| vec2 redUv = vUv + direction * aberration; |
| vec2 blueUv = vUv - direction * aberration; |
| |
| float r = texture2D(tDiffuse, redUv).r; |
| float g = texture2D(tDiffuse, vUv).g; |
| float b = texture2D(tDiffuse, blueUv).b; |
| |
| gl_FragColor = vec4(r, g, b, 1.0); |
| } |
| ` |
| }; |
| |
| const chromaticAberrationPass = new ShaderPass(chromaticAberrationShader); |
| composer.addPass(chromaticAberrationPass); |
| |
| |
| scene.fog = new THREE.FogExp2(0x1a1a2e, 0.02); |
| |
| |
| const skyGeometry = new THREE.SphereGeometry(100, 32, 32); |
| const skyMaterial = new THREE.ShaderMaterial({ |
| uniforms: { |
| topColor: { value: new THREE.Color(0x1a1a2e) }, |
| bottomColor: { value: new THREE.Color(0x2d3748) }, |
| offset: { value: 20 }, |
| exponent: { value: 0.4 } |
| }, |
| vertexShader: ` |
| varying vec3 vWorldPosition; |
| void main() { |
| vec4 worldPosition = modelMatrix * vec4(position, 1.0); |
| vWorldPosition = worldPosition.xyz; |
| gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
| } |
| `, |
| fragmentShader: ` |
| uniform vec3 topColor; |
| uniform vec3 bottomColor; |
| uniform float offset; |
| uniform float exponent; |
| varying vec3 vWorldPosition; |
| |
| void main() { |
| float h = normalize(vWorldPosition + offset).y; |
| gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0); |
| } |
| `, |
| side: THREE.BackSide |
| }); |
| |
| const sky = new THREE.Mesh(skyGeometry, skyMaterial); |
| scene.add(sky); |
| |
| |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.5); |
| scene.add(ambientLight); |
| |
| const keyLight = new THREE.DirectionalLight(0xffffff, 1); |
| keyLight.position.set(5, 10, 5); |
| keyLight.castShadow = true; |
| keyLight.shadow.mapSize.width = 1024; |
| keyLight.shadow.mapSize.height = 1024; |
| keyLight.shadow.camera.near = 0.5; |
| keyLight.shadow.camera.far = 50; |
| keyLight.shadow.camera.left = -10; |
| keyLight.shadow.camera.right = 10; |
| keyLight.shadow.camera.top = 10; |
| keyLight.shadow.camera.bottom = -10; |
| scene.add(keyLight); |
| |
| const rimLight = new THREE.DirectionalLight(0x00ffff, 0.8); |
| rimLight.position.set(-5, 5, -5); |
| scene.add(rimLight); |
| |
| const fillLight = new THREE.DirectionalLight(0xff00ff, 0.6); |
| fillLight.position.set(5, 3, -5); |
| scene.add(fillLight); |
| |
| |
| const crystalMaterial = new THREE.MeshPhysicalMaterial({ |
| color: 0xffffff, |
| metalness: 0.0, |
| roughness: 0.1, |
| transmission: 0.9, |
| thickness: 0.5, |
| ior: 1.5, |
| clearcoat: 1.0, |
| clearcoatRoughness: 0.1, |
| envMapIntensity: 1.0, |
| side: THREE.DoubleSide |
| }); |
| |
| |
| const crystals = []; |
| for (let i = 0; i < 15; i++) { |
| const geometry = new THREE.IcosahedronGeometry(Math.random() * 0.8 + 0.2, 0); |
| const crystal = new THREE.Mesh(geometry, crystalMaterial); |
| |
| crystal.position.set( |
| (Math.random() - 0.5) * 8, |
| Math.random() * 6 + 2, |
| (Math.random() - 0.5) * 8 |
| ); |
| |
| crystal.rotation.set( |
| Math.random() * Math.PI, |
| Math.random() * Math.PI, |
| Math.random() * Math.PI |
| ); |
| |
| crystal.userData = { |
| rotationSpeed: { |
| x: (Math.random() - 0.5) * 0.01, |
| y: (Math.random() - 0.5) * 0.01, |
| z: (Math.random() - 0.5) * 0.01 |
| }, |
| floatSpeed: Math.random() * 0.5 + 0.2, |
| floatOffset: Math.random() * Math.PI * 2 |
| }; |
| |
| crystal.castShadow = true; |
| crystal.receiveShadow = true; |
| |
| scene.add(crystal); |
| crystals.push(crystal); |
| } |
| |
| |
| const gridHelper = new THREE.GridHelper(100, 50, 0x444444, 0x222222); |
| gridHelper.position.y = -1; |
| gridHelper.material.opacity = 0.3; |
| gridHelper.material.transparent = true; |
| scene.add(gridHelper); |
| |
| |
| const satelliteGeometry = new THREE.IcosahedronGeometry(0.5, 0); |
| const satellite = new THREE.Mesh(satelliteGeometry, crystalMaterial); |
| satellite.castShadow = true; |
| scene.add(satellite); |
| |
| |
| const particleCount = 100; |
| const particles = new THREE.BufferGeometry(); |
| const positions = new Float32Array(particleCount * 3); |
| const opacities = new Float32Array(particleCount); |
| |
| particles.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
| particles.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1)); |
| |
| const particleMaterial = new THREE.PointsMaterial({ |
| color: 0x00ffff, |
| size: 0.1, |
| transparent: true, |
| opacity: 0.6, |
| blending: THREE.AdditiveBlending |
| }); |
| |
| const particleSystem = new THREE.Points(particles, particleMaterial); |
| scene.add(particleSystem); |
| |
| |
| let mouseX = 0, mouseY = 0; |
| const targetMouseX = 0, targetMouseY = 0; |
| |
| document.addEventListener('mousemove', (event) => { |
| targetMouseX = (event.clientX / window.innerWidth) * 2 - 1; |
| targetMouseY = -(event.clientY / window.innerHeight) * 2 + 1; |
| }); |
| |
| |
| const clock = new THREE.Clock(); |
| |
| function animate() { |
| requestAnimationFrame(animate); |
| |
| const time = clock.getElapsedTime(); |
| |
| |
| mouseX += (targetMouseX - mouseX) * 0.05; |
| mouseY += (targetMouseY - mouseY) * 0.05; |
| |
| camera.position.x += (mouseX * 2 - camera.position.x) * 0.05; |
| camera.position.y += (5 + mouseY * 2 - camera.position.y) * 0.05; |
| camera.lookAt(0, 2, 0); |
| |
| |
| crystals.forEach(crystal => { |
| crystal.rotation.x += crystal.userData.rotationSpeed.x; |
| crystal.rotation.y += crystal.userData.rotationSpeed.y; |
| crystal.rotation.z += crystal.userData.rotationSpeed.z; |
| |
| crystal.position.y += Math.sin(time * crystal.userData.floatSpeed + crystal.userData.floatOffset) * 0.002; |
| }); |
| |
| |
| const orbitRadius = 10; |
| const orbitSpeed = 0.5; |
| satellite.position.x = Math.cos(time * orbitSpeed) * orbitRadius; |
| satellite.position.z = Math.sin(time * orbitSpeed) * orbitRadius; |
| satellite.position.y = 5 + Math.sin(time * 0.3) * 2; |
| |
| satellite.rotation.x += 0.01; |
| satellite.rotation.y += 0.02; |
| |
| |
| const positions = particles.attributes.position.array; |
| const opacities = particles.attributes.opacity.array; |
| |
| for (let i = 0; i < particleCount; i++) { |
| const t = i / particleCount; |
| const trailIndex = Math.floor(t * 20) % 20; |
| const trailTime = time - trailIndex * 0.1; |
| |
| const trailRadius = orbitRadius; |
| positions[i * 3] = Math.cos(trailTime * orbitSpeed) * trailRadius; |
| positions[i * 3 + 1] = 5 + Math.sin(trailTime * 0.3) * 2; |
| positions[i * 3 + 2] = Math.sin(trailTime * orbitSpeed) * trailRadius; |
| |
| opacities[i] = (1 - t) * 0.6; |
| } |
| |
| particles.attributes.position.needsUpdate = true; |
| particles.attributes.opacity.needsUpdate = true; |
| |
| composer.render(); |
| } |
| |
| |
| window.addEventListener('resize', () => { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| composer.setSize(window.innerWidth, window.innerHeight); |
| }); |
| |
| animate(); |
| </script> |
| </body> |
| </html> |