Created
June 2, 2026 05:30
-
-
Save EncodeTheCode/5ba17147b9b035bc9488fcd0d98d57b9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>PS1-Style Crash Crystal Prototype</title> | |
| <style> | |
| html, body { | |
| margin: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| background: #120816; | |
| } | |
| canvas { display: block; } | |
| .label { | |
| position: fixed; | |
| left: 12px; | |
| top: 12px; | |
| color: rgba(255,255,255,0.78); | |
| font: 12px/1.4 system-ui, sans-serif; | |
| user-select: none; | |
| pointer-events: none; | |
| text-shadow: 0 1px 2px rgba(0,0,0,0.7); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="label">Three.js prototype: faceted crystal with silhouette-only red glow</div> | |
| <script type="module"> | |
| import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x120816); | |
| scene.fog = new THREE.Fog(0x120816, 7, 18); | |
| const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); | |
| camera.position.set(0, 0.25, 5.9); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| renderer.sortObjects = true; | |
| document.body.appendChild(renderer.domElement); | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.72)); | |
| const key = new THREE.DirectionalLight(0xffffff, 1.2); | |
| key.position.set(-1.2, 2.0, 3.2); | |
| scene.add(key); | |
| const rim = new THREE.DirectionalLight(0xff99bb, 0.45); | |
| rim.position.set(2.3, -0.3, 2.1); | |
| scene.add(rim); | |
| const group = new THREE.Group(); | |
| scene.add(group); | |
| function makeCrystalGeometry() { | |
| const top = 1.48; | |
| const crown = 1.18; | |
| const upper = 0.70; | |
| const lower = -0.72; | |
| const bottom = -1.52; | |
| const p = [ | |
| [0.00, top, 0.00], | |
| [-0.30, crown, 0.18], | |
| [ 0.18, crown, 0.32], | |
| [ 0.40, crown, -0.06], | |
| [-0.08, crown, -0.34], | |
| [-0.54, upper, 0.28], | |
| [ 0.38, upper, 0.42], | |
| [ 0.60, upper, -0.10], | |
| [-0.16, upper, -0.56], | |
| [-0.44, lower, 0.18], | |
| [ 0.26, lower, 0.30], | |
| [ 0.44, lower, -0.12], | |
| [-0.08, lower, -0.42], | |
| [0.00, bottom, 0.00], | |
| ]; | |
| const faces = [ | |
| [0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 1], | |
| [1, 5, 6], [1, 6, 2], [2, 6, 7], [2, 7, 3], [3, 7, 8], [3, 8, 4], [4, 8, 5], [4, 5, 1], | |
| [5, 9, 10], [5, 10, 6], [6, 10, 11], [6, 11, 7], [7, 11, 12], [7, 12, 8], [8, 12, 9], [8, 9, 5], | |
| [9, 13, 10], [10, 13, 11], [11, 13, 12], [12, 13, 9], | |
| ]; | |
| const positions = []; | |
| const normals = []; | |
| for (const [a, b, c] of faces) { | |
| const pa = new THREE.Vector3(...p[a]); | |
| const pb = new THREE.Vector3(...p[b]); | |
| const pc = new THREE.Vector3(...p[c]); | |
| const ab = new THREE.Vector3().subVectors(pb, pa); | |
| const ac = new THREE.Vector3().subVectors(pc, pa); | |
| const normal = new THREE.Vector3().crossVectors(ab, ac).normalize(); | |
| positions.push(pa.x, pa.y, pa.z, pb.x, pb.y, pb.z, pc.x, pc.y, pc.z); | |
| for (let i = 0; i < 3; i++) normals.push(normal.x, normal.y, normal.z); | |
| } | |
| const geom = new THREE.BufferGeometry(); | |
| geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); | |
| geom.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); | |
| geom.computeBoundingSphere(); | |
| return geom; | |
| } | |
| const crystalGeo = makeCrystalGeometry(); | |
| // Three stacked shell passes create a softer, more diffuse glow without any spherical halo. | |
| // Because they use BackSide and remain behind the crystal, the glow only leaks out | |
| // along silhouette edges and corners from the viewer's perspective. | |
| const glowMatA = new THREE.ShaderMaterial({ | |
| transparent: true, | |
| depthWrite: false, | |
| depthTest: true, | |
| blending: THREE.AdditiveBlending, | |
| side: THREE.BackSide, | |
| uniforms: { | |
| glowColor: { value: new THREE.Color(0xff5776) }, | |
| glowPower: { value: 2.0 }, | |
| glowIntensity: { value: 0.465 }, | |
| scaleAdd: { value: 0.050 }, | |
| }, | |
| vertexShader: ` | |
| uniform float scaleAdd; | |
| varying vec3 vNormal; | |
| varying vec3 vWorldPosition; | |
| void main() { | |
| vec3 displaced = position + normal * scaleAdd; | |
| vNormal = normalize(mat3(modelMatrix) * normal); | |
| vec4 worldPosition = modelMatrix * vec4(displaced, 1.0); | |
| vWorldPosition = worldPosition.xyz; | |
| gl_Position = projectionMatrix * viewMatrix * worldPosition; | |
| } | |
| `, | |
| fragmentShader: ` | |
| uniform vec3 glowColor; | |
| uniform float glowPower; | |
| uniform float glowIntensity; | |
| varying vec3 vNormal; | |
| varying vec3 vWorldPosition; | |
| void main() { | |
| vec3 viewDir = normalize(cameraPosition - vWorldPosition); | |
| float fresnel = pow(1.0 - max(dot(normalize(vNormal), viewDir), 0.0), glowPower); | |
| float alpha = fresnel * glowIntensity; | |
| gl_FragColor = vec4(glowColor, alpha); | |
| } | |
| `, | |
| }); | |
| const glowMatB = glowMatA.clone(); | |
| glowMatB.uniforms = THREE.UniformsUtils.clone(glowMatA.uniforms); | |
| glowMatB.uniforms.glowIntensity.value = 0.255; | |
| glowMatB.uniforms.scaleAdd.value = 0.085; | |
| glowMatB.uniforms.glowPower.value = 1.65; | |
| const glowMatC = glowMatA.clone(); | |
| glowMatC.uniforms = THREE.UniformsUtils.clone(glowMatA.uniforms); | |
| glowMatC.uniforms.glowIntensity.value = 0.135; | |
| glowMatC.uniforms.scaleAdd.value = 0.125; | |
| glowMatC.uniforms.glowPower.value = 1.45; | |
| const glowShell1 = new THREE.Mesh(crystalGeo.clone(), glowMatA); | |
| const glowShell2 = new THREE.Mesh(crystalGeo.clone(), glowMatB); | |
| const glowShell3 = new THREE.Mesh(crystalGeo.clone(), glowMatC); | |
| glowShell1.renderOrder = 0; | |
| glowShell2.renderOrder = 0.1; | |
| glowShell3.renderOrder = 0.2; | |
| group.add(glowShell1, glowShell2, glowShell3); | |
| const crystalMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x8d3cff, | |
| emissive: 0x220014, | |
| emissiveIntensity: 1.78, | |
| roughness: 0.35, | |
| metalness: 0.25, | |
| flatShading: true, | |
| }); | |
| const crystal = new THREE.Mesh(crystalGeo, crystalMaterial); | |
| crystal.renderOrder = 1; | |
| group.add(crystal); | |
| const shell = new THREE.Mesh( | |
| crystalGeo.clone(), | |
| new THREE.MeshBasicMaterial({ | |
| color: 0x26102f, | |
| side: THREE.BackSide, | |
| transparent: true, | |
| opacity: 0.42, | |
| }) | |
| ); | |
| shell.scale.setScalar(1.02); | |
| shell.renderOrder = 0.3; | |
| group.add(shell); | |
| group.rotation.x = -0.05; | |
| group.rotation.z = 0.06; | |
| function animate(t) { | |
| const time = t * 0.001; | |
| const bob = Math.sin(time * 2.1) * 0.04; | |
| const pulse = 1 + Math.sin(time * 3.0) * 0.012; | |
| group.rotation.y = time * 0.8; | |
| crystal.rotation.y = time * 0.8; | |
| shell.rotation.y = time * 0.8; | |
| glowShell1.rotation.y = time * 0.8; | |
| glowShell2.rotation.y = time * 0.8; | |
| glowShell3.rotation.y = time * 0.8; | |
| crystal.position.y = bob; | |
| shell.position.y = bob; | |
| glowShell1.position.y = bob; | |
| glowShell2.position.y = bob; | |
| glowShell3.position.y = bob; | |
| crystal.scale.setScalar(pulse); | |
| shell.scale.setScalar(pulse * 1.02); | |
| glowShell1.scale.setScalar(pulse * 1.05); | |
| glowShell2.scale.setScalar(pulse * 1.05); | |
| glowShell3.scale.setScalar(pulse * 1.05); | |
| renderer.render(scene, camera); | |
| requestAnimationFrame(animate); | |
| } | |
| requestAnimationFrame(animate); | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment