<body>
  <div class="description">Click anywhere to move your position.
    <br>Works best on PC.</br>
  Cone FOV slider:
    <input id="fov-slider" type="range" min="0" max="179.9" value="20" id="slider">
  </div>
</body>
body {
  margin: 0;
}
.description {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  color: #3377ff;
  padding: 10px;
  text-align: center;
}
import * as THREE from "https://esm.sh/three";
import { OrbitControls } from "https://esm.sh/three/examples/jsm/controls/OrbitControls";

const vertCode = `
out vec2 vUv; // to send to fragment shader

void main() {
    // Compute view direction in world space
    vec4 worldPos = modelViewMatrix * vec4(position, 1.0);
    vec3 viewDir = normalize(-worldPos.xyz);

    // Output vertex position
    gl_Position = projectionMatrix * worldPos;

    vUv = uv;
}`

const fragCode = `
precision mediump float;

// From vertex shader
in vec2 vUv;

// From uniforms
uniform float u_steps;
uniform float u_maxDis;
uniform float u_eps;

uniform vec2 u_mousePos;
uniform bool u_mouseClick;
uniform vec2 u_currentPos;

uniform float u_ratio;
uniform float u_tanFov;

// Helpers
#define PI 3.1415926538

float gt(float v1, float v2)
{
    return step(v2,v1);
}

float lt(float v1, float v2)
{
    return step(v1, v2);
}

float between(float val, float start, float end)
{
    return gt(val,start)*lt(val,end);
}

float eq(float v1, float v2, float e)
{
    return between(v1, v2-e, v2+e);
}

float s_gt(float v1, float v2, float e)
{
    return smoothstep(v2-e, v2+e, v1);
}

float s_lt(float v1, float v2, float e)
{
    return smoothstep(v1-e, v1+e, v2);
}

float s_between(float val, float start, float end, float epsilon)
{
    return s_gt(val,start,epsilon)*s_lt(val,end,epsilon);
}

float s_eq(float v1, float v2, float e, float s_e)
{
    return s_between(v1, v2-e, v2+e, s_e);
}

float dot2(vec2 v) {
    return dot(v, v);
}

// SDFs from https://iquilezles.org/articles/distfunctions2d/
float sdCircle(vec2 p, float r) {
    return length(p) - r;
}

float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
    vec2 pa = p-a, ba = b-a;
    float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
    return length( pa - ba*h );
}

float sdStar5(in vec2 p, in float r, in float rf)
{
    const vec2 k1 = vec2(0.809016994375, -0.587785252292);
    const vec2 k2 = vec2(-k1.x,k1.y);
    p.x = abs(p.x);
    p -= 2.0*max(dot(k1,p),0.0)*k1;
    p -= 2.0*max(dot(k2,p),0.0)*k2;
    p.x = abs(p.x);
    p.y -= r;
    vec2 ba = rf*vec2(-k1.y,k1.x) - vec2(0,1);
    float h = clamp( dot(p,ba)/dot(ba,ba), 0.0, r );
    return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}

float sdMoon(vec2 p, float d, float ra, float rb )
{
    p.y = abs(p.y);
    float a = (ra*ra - rb*rb + d*d)/(2.0*d);
    float b = sqrt(max(ra*ra-a*a,0.0));
    if( d*(p.x*b-p.y*a) > d*d*max(b-p.y,0.0) )
          return length(p-vec2(a,b));
    return max( (length(p          )-ra),
               -(length(p-vec2(d,0))-rb));
}

float sdHeart( in vec2 p )
{
    p.x = abs(p.x);

    if( p.y+p.x>1.0 ) return sqrt(dot2(p-vec2(0.25,0.75))) - sqrt(2.0)/4.0;
    return sqrt(min(dot2(p-vec2(0.00,1.00)),
                    dot2(p-0.5*max(p.x+p.y,0.0)))) * sign(p.x-p.y);
}

// Render functions for SDFs
float circleRender(vec2 p, float r) {
    return s_lt(length(p), r, 0.005);
}

float hollowCircleRender(vec2 p, float r, float t) {
    return s_between(length(p), r-t, r, 0.005);
}

float segmentRender(vec2 p, vec2 a, vec2 b, float w) {
    return s_lt(sdSegment(p, a, b), w, 0.005);
}

float scene(vec2 xy) {
    float s1 = sdCircle(xy-vec2(1,1), 0.5);
    float s2 = sdCircle(xy-vec2(-1,-1), 0.5);
    float heart = sdHeart(xy-vec2(-1,0.5));
    float moon = sdMoon(xy-vec2(-1,-1), 0.5, 0.75, 0.6);
    return min(s1, min(s2, min(heart, moon)));
}


void main() {
    // Get UV from vertex shader
    vec2 uv = vUv.xy;
    
    // Setting up view port
    float zoom = 8.;
    vec2 zoomCenter = vec2(0., 0.);
    vec2 viewPortCenter = vec2(0.5, 0.5);

    // Establishing mouse xy values
    vec2 mouse = u_mousePos;
    mouse.x *= zoom/2.;
    mouse.y *= zoom/2. * u_ratio;

    // Establishing current xy values
    vec2 current = u_currentPos;
    current.x *= zoom/2.;
    current.y *= zoom/2. * u_ratio;

    // Establishing screen xy values
    vec2 xy = (uv - viewPortCenter) * zoom + zoomCenter;
    xy = vec2(xy.x, xy.y*u_ratio);    

    vec3 col = vec3(0);

    // Cone center definition
    vec2 crd = normalize(mouse - current); // cone ray direction 
    vec2 crdPerp = vec2(-crd.y, crd.x); // vector perpendicular to cone ray direction
    vec2 cro = current; // cone ray origin

    // current position
    col.r += circleRender(cro-xy, 0.1);

    // Render Scene
    float sceneDis = scene(xy);
    col += sin(sceneDis*100.)*0.2;
    col += s_lt(sceneDis, 0., 0.005);

    col = max(col, vec3(0));

    // Cone marching
    float d = 0.;
    float cd; // current distance
    float ccr; // current cone radius
    vec2 p;
    for (int i = 0; i < int(u_steps); i++) {
        p = cro + d * crd;
        cd = scene(p);
        ccr = (d * u_tanFov);

        col.g += circleRender(p-xy, 0.025);
        col += hollowCircleRender(p-xy, cd, 0.01);

        col.g += segmentRender(xy, p + cd * crdPerp, p - cd * crdPerp, 0.01);

        if (cd < ccr || cd < u_eps || d > u_maxDis) break;
        d += cd;
    }

    col.r += segmentRender(xy, cro, p, 0.01);

    if (d < u_maxDis) col.b += circleRender(p-xy, 0.05);

    // mouse position
    col.r += circleRender(mouse-xy, 0.1);

    // Render Cone
    vec2 coneLeft = p + ccr * crdPerp;
    vec2 coneRight = p - ccr * crdPerp;

    col.rb += segmentRender(xy, coneLeft, coneRight, 0.01);
    col.rgb += segmentRender(xy, cro, coneRight, 0.01);
    col.rgb += segmentRender(xy, cro, coneLeft, 0.01);

    float coneRadiusInf = 10. * u_tanFov;
    vec2 coneEndPointInf = cro + 10. * crd;
    vec2 coneLeftInf = coneEndPointInf + coneRadiusInf * crdPerp;
    vec2 coneRightInf = coneEndPointInf - coneRadiusInf * crdPerp;

    col.rg += segmentRender(xy, cro, coneRightInf, 0.01);
    col.rg += segmentRender(xy, cro, coneLeftInf, 0.01);


    gl_FragColor = vec4(col ,1);
}
`

// Create a scene
const scene = new THREE.Scene();

// Create a camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// Create a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Set background color
const backgroundColor = new THREE.Color(0x3399ee);
renderer.setClearColor(backgroundColor, 1);

// Add orbit controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxDistance = 10;
controls.minDistance = 2;
controls.enableDamping = true;

// Add directional light
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
scene.add(light);

// Create a ray marching plane
const geometry = new THREE.PlaneGeometry();
const material = new THREE.ShaderMaterial();
const rayMarchPlane = new THREE.Mesh(geometry, material);

// Get the wdith and height of the near plane
const nearPlaneWidth = camera.near * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.aspect * 2;
const nearPlaneHeight = nearPlaneWidth / camera.aspect;

// Scale the ray marching plane
rayMarchPlane.scale.set(nearPlaneWidth, nearPlaneHeight, 1);

// Add uniforms
const uniforms = {
  u_steps: { value: 100 },
  u_maxDis: { value: 10 },
  u_eps: { value: 0.001 },

  u_mousePos: { value: new THREE.Vector2() },
  u_mouseClick: { value: false },
  u_currentPos: { value: new THREE.Vector2(0.5,-0.5) },
  
  u_ratio: { value: window.innerHeight / window.innerWidth },
  
  u_tanFov: { value: Math.tan(THREE.MathUtils.degToRad(20 / 2)) }
};
material.uniforms = uniforms;

material.vertexShader = vertCode;
material.fragmentShader = fragCode;

// Add plane to scene
scene.add(rayMarchPlane);

// Needed inside update function
let cameraForwardPos = new THREE.Vector3(0, 0, -1);
const VECTOR3ZERO = new THREE.Vector3(0, 0, 0);

// Render the scene
const animate = () => {
  requestAnimationFrame(animate);

  // Update screen plane position and rotation
  cameraForwardPos = camera.position.clone().add(camera.getWorldDirection(VECTOR3ZERO).multiplyScalar(camera.near));
  rayMarchPlane.position.copy(cameraForwardPos);
  rayMarchPlane.rotation.copy(camera.rotation);

  renderer.render(scene, camera);

  controls.update();
}
animate();

// Handle window resize
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  const nearPlaneWidth = camera.near * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.aspect * 2;
  const nearPlaneHeight = nearPlaneWidth / camera.aspect;
  rayMarchPlane.scale.set(nearPlaneWidth, nearPlaneHeight, 1);

  renderer.setSize(window.innerWidth, window.innerHeight);

  // Update screen ratio uniform
  uniforms.u_ratio.value = window.innerHeight / window.innerWidth;
});

// Handle mouse move
window.addEventListener('mousemove', (event) => {
  // Update mouse position uniform
  uniforms.u_mousePos.value.x = (event.clientX / window.innerWidth) * 2 - 1;
  uniforms.u_mousePos.value.y = -(event.clientY / window.innerHeight) * 2 + 1;
});

// Handle mouse clicks
window.addEventListener('mousedown', (event) => {
  if (event.button === 0) {
    uniforms.u_mouseClick.value = true;
    uniforms.u_currentPos.value.x = uniforms.u_mousePos.value.x;
    uniforms.u_currentPos.value.y = uniforms.u_mousePos.value.y;
  }
});

window.addEventListener('mouseup', (event) => {
  uniforms.u_mouseClick.value = false;
});

// Handle slider change
const slider = document.getElementById("fov-slider");
slider.addEventListener("input", (event) => {
  const fov = parseFloat(event.target.value);
  uniforms.u_tanFov.value = Math.tan(THREE.MathUtils.degToRad(fov / 2));
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.