<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));
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.