Created
October 30, 2025 22:45
-
-
Save twobob/dee58e84d0532870579ab3a3d3ac3210 to your computer and use it in GitHub Desktop.
Tunnel Vision Dissolve Shader
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>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> |
Author
twobob
commented
Oct 30, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment