Skip to content

Instantly share code, notes, and snippets.

@twobob
Created October 30, 2025 22:45
Show Gist options
  • Select an option

  • Save twobob/dee58e84d0532870579ab3a3d3ac3210 to your computer and use it in GitHub Desktop.

Select an option

Save twobob/dee58e84d0532870579ab3a3d3ac3210 to your computer and use it in GitHub Desktop.
Tunnel Vision Dissolve Shader
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tunnel Vision Dissolve Shader</title>
<style>
body { margin: 0; overflow: hidden; background-color: #add8e6; }
canvas { display: block; }
.info {
position: absolute;
top: 10px;
left: 10px;
color: #333;
font-family: sans-serif;
background: rgba(255,255,255,0.7);
padding: 10px;
border-radius: 5px;
max-width: 350px;
z-index: 100;
}
</style>
</head>
<body>
<div class="info">
<strong>Tunnel Vision Dissolve Shader</strong><br>
<b>Controls:</b><br>
- <b>WASD</b>: Move player<br>
- <b>Left-drag</b>: Orbit camera<br>
- <b>Scroll</b>: Zoom in/out<br>
Objects blocking the view between camera and player will dissolve, creating a clear tunnel of visibility.
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// --- Basic Scene Setup ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x94D5E0);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 20);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// --- Lighting ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
// --- Simple Orbit Controls ---
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
let cameraRotation = { x: 0.2, y: 0 };
let cameraDistance = 20;
const target = new THREE.Vector3(0, 2, 0);
// WASD movement
const keys = {};
const playerSpeed = 0.15;
window.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
});
window.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
function updatePlayerPosition() {
let moved = false;
// Calculate camera's forward and right directions (on XZ plane)
const forward = new THREE.Vector3();
const right = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0; // Keep movement on ground plane
forward.normalize();
right.crossVectors(forward, new THREE.Vector3(0, 1, 0));
right.normalize();
if (keys['w']) {
target.x += forward.x * playerSpeed;
target.z += forward.z * playerSpeed;
moved = true;
}
if (keys['s']) {
target.x -= forward.x * playerSpeed;
target.z -= forward.z * playerSpeed;
moved = true;
}
if (keys['a']) {
target.x -= right.x * playerSpeed;
target.z -= right.z * playerSpeed;
moved = true;
}
if (keys['d']) {
target.x += right.x * playerSpeed;
target.z += right.z * playerSpeed;
moved = true;
}
if (moved) {
// Update player mesh position
player.position.copy(target);
pole.position.copy(target);
}
}
renderer.domElement.addEventListener('mousedown', (e) => {
isDragging = true;
previousMousePosition = { x: e.clientX, y: e.clientY };
});
renderer.domElement.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaX = e.clientX - previousMousePosition.x;
const deltaY = e.clientY - previousMousePosition.y;
cameraRotation.y += deltaX * 0.005;
cameraRotation.x += deltaY * 0.005;
cameraRotation.x = Math.max(-Math.PI/2 + 0.1, Math.min(Math.PI/2 - 0.1, cameraRotation.x));
previousMousePosition = { x: e.clientX, y: e.clientY };
}
});
renderer.domElement.addEventListener('mouseup', () => {
isDragging = false;
});
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
cameraDistance += e.deltaY * 0.02;
cameraDistance = Math.max(8, Math.min(50, cameraDistance));
});
function updateCamera() {
const x = target.x + cameraDistance * Math.sin(cameraRotation.y) * Math.cos(cameraRotation.x);
const y = target.y + cameraDistance * Math.sin(cameraRotation.x);
const z = target.z + cameraDistance * Math.cos(cameraRotation.y) * Math.cos(cameraRotation.x);
camera.position.set(x, y, z);
camera.lookAt(target);
}
// --- Shaders ---
const vertexShader = `
varying vec3 vWorldPosition;
varying vec3 vNormal;
void main() {
vNormal = normal;
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform sampler2D noiseTexture;
uniform float dissolveThreshold;
uniform vec3 targetPosition;
uniform float tunnelRadius;
varying vec3 vWorldPosition;
varying vec3 vNormal;
float triplanarNoise(vec3 worldPos, float scale) {
vec3 blending = abs(vNormal);
blending = normalize(max(blending, 0.00001));
blending /= (blending.x + blending.y + blending.z);
float xz = texture2D(noiseTexture, worldPos.xz * scale).r;
float xy = texture2D(noiseTexture, worldPos.xy * scale).r;
float yz = texture2D(noiseTexture, worldPos.yz * scale).r;
return xz * blending.y + xy * blending.z + yz * blending.x;
}
// Calculate distance from point to line segment
float distanceToLineSegment(vec3 point, vec3 lineStart, vec3 lineEnd) {
vec3 line = lineEnd - lineStart;
float lineLength = length(line);
vec3 lineDir = line / lineLength;
vec3 toPoint = point - lineStart;
float t = dot(toPoint, lineDir);
t = clamp(t, 0.0, lineLength);
vec3 closestPoint = lineStart + lineDir * t;
return distance(point, closestPoint);
}
void main() {
float noise = triplanarNoise(vWorldPosition, 0.2);
// Calculate distance from this pixel to the camera-target line
float distToLine = distanceToLineSegment(vWorldPosition, cameraPosition, targetPosition);
// Create dissolve factor based on proximity to the sight line
float dissolveFactor = 1.0 - smoothstep(tunnelRadius * 0.5, tunnelRadius * 2.0, distToLine);
float finalThreshold = dissolveFactor * dissolveThreshold;
if (noise < finalThreshold) {
discard;
}
float edgeWidth = 0.08;
float edgeFactor = smoothstep(finalThreshold, finalThreshold + edgeWidth, noise);
vec3 baseColor = vec3(0.1, 0.3, 0.9);
vec3 edgeColor = vec3(0.4, 0.7, 1.0);
vec3 finalColor = mix(edgeColor, baseColor, edgeFactor);
gl_FragColor = vec4(finalColor, 1.0);
}
`;
// --- Create Procedural Noise Texture ---
const size = 128;
const data = new Uint8Array(size * size);
for (let i = 0; i < data.length; i++) {
data[i] = Math.random() * 255;
}
const noiseTexture = new THREE.DataTexture(data, size, size, THREE.LuminanceFormat);
noiseTexture.wrapS = THREE.RepeatWrapping;
noiseTexture.wrapT = THREE.RepeatWrapping;
noiseTexture.needsUpdate = true;
// --- Custom Shader Material ---
const dissolveMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
noiseTexture: { value: noiseTexture },
dissolveThreshold: { value: 1.0 },
targetPosition: { value: new THREE.Vector3(0, 2, 0) },
tunnelRadius: { value: 3.0 }
},
side: THREE.DoubleSide
});
// --- Geometry ---
// Floor
const floorGeometry = new THREE.PlaneGeometry(50, 50);
const floorMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// Central Target object (doesn't dissolve) - make it taller so it's visible
const playerGeometry = new THREE.SphereGeometry(0.7, 16, 12);
const playerMaterial = new THREE.MeshStandardMaterial({
color: 0xff4500,
emissive: 0xff2200,
emissiveIntensity: 0.3
});
const player = new THREE.Mesh(playerGeometry, playerMaterial);
player.position.set(0, 2, 0);
scene.add(player);
// Add a pole to make the target more visible
const poleGeometry = new THREE.CylinderGeometry(0.2, 0.2, 4, 8);
const pole = new THREE.Mesh(poleGeometry, playerMaterial);
pole.position.set(0, 2, 0);
scene.add(pole);
// --- Random Geometry Generation ---
const geometries = [
new THREE.BoxGeometry(1, 1, 1),
new THREE.SphereGeometry(1, 16, 12),
new THREE.ConeGeometry(1, 2, 16),
new THREE.CylinderGeometry(0.8, 0.8, 2, 16),
new THREE.TorusKnotGeometry(0.8, 0.25, 64, 8)
];
const numObjects = 80;
const spread = 30;
for (let i = 0; i < numObjects; i++) {
const randomGeometry = geometries[Math.floor(Math.random() * geometries.length)];
const mesh = new THREE.Mesh(randomGeometry, dissolveMaterial);
mesh.position.x = (Math.random() - 0.5) * spread;
mesh.position.z = (Math.random() - 0.5) * spread;
const randomScale = 0.8 + Math.random() * 2.5;
mesh.scale.set(randomScale, randomScale, randomScale);
mesh.position.y = randomScale * 1.2 + Math.random() * 3;
mesh.rotation.x = Math.random() * 2 * Math.PI;
mesh.rotation.y = Math.random() * 2 * Math.PI;
mesh.rotation.z = Math.random() * 2 * Math.PI;
scene.add(mesh);
}
// --- Handle Window Resizing ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
updatePlayerPosition();
updateCamera();
// Update shader uniforms
dissolveMaterial.uniforms.targetPosition.value.copy(target);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
@twobob
Copy link
Author

twobob commented Oct 30, 2025

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment