Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created June 2, 2026 05:30
Show Gist options
  • Select an option

  • Save EncodeTheCode/5ba17147b9b035bc9488fcd0d98d57b9 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/5ba17147b9b035bc9488fcd0d98d57b9 to your computer and use it in GitHub Desktop.
<!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