SDFs in the scene - raymarching

After solving the problem in the question post Raycast for meshes and SDFs here is an example of moving meshes and SDFs via raycast.

07_SDF_ShaderSoloV13

Handling is sometimes a little tricky when it comes to selecting the elements. To rotate the auxiliary plane, you have to zoom out and rotate the box.


The Code

<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  --> 
<html>
<head>
  <title>07_SDF_ShaderSoloV13</title>
  <meta charset="utf-8" />
  <style>
    body {
    overflow: hidden;
    margin: 0;
    text-align: center;
    }
  </style>
</head>
<body>
    spacing <input id="spacing" type="number" min="0.0" step="0.05" max="2.0" value="1.0"> </br>
</body>

<script type="module">
  
// @author hofk
import * as THREE from "../../jsm/three.module.173.js";
import { OrbitControls } from "../../jsm/OrbitControls.173.js";

let space = 1.0;
document.getElementById('spacing').addEventListener( 'input', (e) => {space = e.target.value});

window.addEventListener('mousedown', onDocumentMouseDown, false);
window.addEventListener('mousemove', onDocumentMouseMove, false);
window.addEventListener('mouseup'  , onDocumentMouseUp  , false);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.01, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0xdedede);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(1, 4, 100);

const light = new THREE.AmbientLight(0x404040, 4.5); // soft white light
scene.add(light);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5);
directionalLight.position.set(5, 15, 15);
scene.add(directionalLight);
const controls = new OrbitControls(camera, renderer.domElement);
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);

let selectionMesh;
let selectionSDF;
let offsetMesh 	= new THREE.Vector3();
 
let objectsToRaycast = [];
const raycasterSDF = new THREE.Raycaster();
const raycasterMesh = new THREE.Raycaster();

const mouse = new THREE.Vector2();

// Auxiliary layer for determining the mouse position and moving the clicked object in 3D
const auxPlaneGeo = new THREE.PlaneGeometry( 80, 80, 1, 1 );
const auxPlaneMat = new THREE.MeshBasicMaterial({color:0xaaaaaa, transparent:true, opacity:0.05 , side:THREE.DoubleSide, wireframe:true});
const auxPlane = new THREE.Mesh(auxPlaneGeo, auxPlaneMat); 
scene.add( auxPlane );
		
const cyl = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 2 ), new THREE.MeshPhongMaterial( { color: 0x00ff00, wireframe:false })); 
objectsToRaycast.push(cyl);
scene.add(cyl);
const ico = new THREE.Mesh(new THREE.IcosahedronGeometry( 1, 4), new THREE.MeshPhongMaterial( { color: 0x00ffff, wireframe:false })); 
objectsToRaycast.push(ico);
ico.translateY( 1 );
scene.add(ico);
 
// Vertex Shader
const vShader = `
  varying vec3 vPosition;
  varying vec2 vUv;
  void main() {
    vPosition = position;
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }
`;

// Fragment Shader
const fShader = `
uniform float time;
uniform float boundingRadius;
uniform float space;
uniform vec3 camPos;
uniform vec3 mousePosition;
uniform vec2 resolution;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec3 vPosition;
varying vec2 vUv;

#define MAX_STEPS 250
#define MAX_DIST 100.0
#define SURF_DIST 1e-4
#define PI 3.1415926

// distance color
struct distCol {
    float d;
    vec4 c;
};

float sdSphere( vec3 p, float s )
{
    return length(p)-s;
}

vec3 translateXYZ(vec3 p, vec3 q) {
    return p - q;
}

distCol opSmoothUnion( distCol dc1, distCol dc2, float k ){
  distCol dc;
  float h = clamp( 0.5 + 0.5*(dc2.d-dc1.d)/k, 0.0, 1.0 );
  float d = mix( dc2.d, dc1.d, h ) - k*h*(1.0-h);
  vec4 c = d < dc2.d ? dc1.c : dc2.c; 
  dc.d = d;
  dc.c = c;     
  return dc;
}
 
distCol GetDist(vec3 p) {
    distCol dc;
    vec3 pMouse;
    
    distCol dcSphereA;
    pMouse = translateXYZ(p, mousePosition);
    dcSphereA.d = sdSphere( pMouse, boundingRadius);
    dcSphereA.c = vec4(1.0, 0.0, 0.0, 1.0);
    
    distCol dcSphereB;
    pMouse = translateXYZ(p, mousePosition + vec3(space));
    dcSphereB.d = sdSphere( pMouse, boundingRadius * 0.8);
    dcSphereB.c = vec4(1.0, 1.0, 0.0, 1.0); 
    
    dc = dcSphereA; // apply to reserved dc
    dc = opSmoothUnion(dc, dcSphereB, 0.6 );
    
    return dc;
}

distCol RayMarch(vec3 ro, vec3 rd) {
    distCol dc;
    float dO = 0.0;
    for (int i = 0; i < MAX_STEPS; i++) {
        vec3 p = ro + rd * dO;
        dc = GetDist(p);
        dO += dc.d;
        if (dO > MAX_DIST || dc.d < SURF_DIST) break;
    }
    dc.d = dO;
    return dc;
}

vec3 GetNormal(vec3 p) {
    float d = GetDist(p).d;
    vec2 e = vec2(SURF_DIST, 0.0);
    float d1 = GetDist(p - e.xyy).d;
    float d2 = GetDist(p - e.yxy).d;
    float d3 = GetDist(p - e.yyx).d;
    vec3 n = d - vec3(d1, d2, d3);
    return normalize(n);
}

float GetAo(vec3 p, vec3 n) {
    float occ = 0.0;
    float sca = 1.0;
    for (int i = 0; i < 5; i++) {
        float h = 0.001 + 0.15 * float(i) / 4.0;
        float d = GetDist(p + h * n).d;
        occ += (h - d) * sca;
        sca *= 0.95;
    }
    return clamp(1.0 - 1.5 * occ, 0.0, 1.0);
}

float GetLight(vec3 p, vec3 lPos) {
    vec3 l = normalize(lPos - p);
    vec3 n = GetNormal(p);
    float dif = clamp(dot(n, l), 0.0, 1.0);
    return dif;
}

void main() {
    vec2 uv = vUv - 0.5;
    vec3 ro = camPos;
    vec3 rd = normalize(vPosition - ro);
    distCol dc = RayMarch(ro, rd);
    
    if (dc.d >= MAX_DIST) {
        gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); // no hit
    } else {
        vec3 p = ro + rd * dc.d;
        vec3 lightPos = vec3(2.0, 16.0, 3.0);
        float diff = GetLight(p, lightPos);
        float ao = 0.051 * GetAo(p, GetNormal(p));
        vec4 ct = dc.c;
        vec3 c = ct.rgb;
        vec3 color = 0.7 * c + 0.5 * diff + 0.2 * ao;
    
        gl_FragColor = vec4(color, ct.a);
    }
}
`;

const boxParam = [100.0, 100.0, 100.0, 0.0, 0.0, 0.0 ];

let boxGeo;
let box;
let shaderMaterial;
let camPos;

boxGeo = new THREE.BoxGeometry(boxParam[0], boxParam[1], boxParam[2]);

//box helper
scene.add(
  new THREE.Box3Helper(
    new THREE.Box3().setFromBufferAttribute(boxGeo.attributes.position),
    0x444444
  )
); 

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: { 
        camPos: { value: new THREE.Vector3().copy(camera.position) },
        mousePosition: { value: new THREE.Vector3() },
        time: { value: 0.0 },
        boundingRadius: { value: 1.2 },
        space:  { value: 1.0 },
        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
    },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: true,  // false,  to display the box
});
box = new THREE.Mesh(boxGeo, shaderMaterial);
scene.add(box);
box.position.set(boxParam[3], boxParam[4], boxParam[5]);
box.renderOrder = Infinity;
camPos = new THREE.Vector3();

controls.addEventListener("change", event => {
    camPos.copy(camera.position);
    box.worldToLocal(camPos);
    shaderMaterial.uniforms.camPos.value.copy(camPos);
}, false);

camPos.copy(camera.position);
box.worldToLocal(camPos);
shaderMaterial.uniforms.camPos.value.copy(camPos);


animate();

function animate( ) {
    
    requestAnimationFrame(animate);
    //shaderMaterial.uniforms.time.value = t;
    shaderMaterial.uniforms.space.value = space;
    renderer.render(scene, camera);
}

function onDocumentMouseDown(event) {
    getMouse( event );
    const vector = new THREE.Vector3(mouse.x, mouse.y, 1);
    vector.unproject(camera);
    raycasterMesh.set(camera.position, vector.sub(camera.position).normalize());
    const intersectsMesh = raycasterMesh.intersectObjects(objectsToRaycast);
    if (intersectsMesh.length > 0) {
       controls.enabled = false;
       selectionMesh = intersectsMesh[0].object;
       const planeIntersects = raycasterMesh.intersectObject(auxPlane);
       if (planeIntersects.length > 0) {
          offsetMesh.copy(planeIntersects[0].point).sub(auxPlane.position);
       }
    }
    else {
       raycasterSDF.setFromCamera(mouse, camera);
       const intersectsSDF = raycasterSDF.intersectObject(box);
       if (intersectsSDF.length > 0) {
          controls.enabled = false;
          selectionSDF = true;
          const planeIntersects = raycasterSDF.intersectObject(auxPlane);
       }
    }  
}

 function onDocumentMouseMove(event) {
    event.preventDefault();
    getMouse( event );
    const vector = new THREE.Vector3(mouse.x, mouse.y, 1);
    vector.unproject(camera);
    raycasterMesh.set(camera.position, vector.sub(camera.position).normalize());
    if (selectionMesh) {
       const intersectsMesh = raycasterMesh.intersectObject(auxPlane);
       if (intersectsMesh.length > 0) {
          let newPos = intersectsMesh[0].point.clone().sub(offsetMesh);
          selectionMesh.position.copy(newPos);
       }
    } else if (selectionSDF) {
       const intersectsSDF = raycasterMesh.intersectObject(auxPlane);
       if (intersectsSDF.length > 0) {
          let newPos = intersectsSDF[0].point.clone();
          let sdfPos = newPos.clone();
          box.worldToLocal(sdfPos);
          box.material.uniforms.mousePosition.value.copy(sdfPos);
       }
    } else {
       const intersectsMesh = raycasterMesh.intersectObjects(objectsToRaycast);
       if (intersectsMesh.length > 0) {
          auxPlane.position.copy(intersectsMesh[0].object.position);
          auxPlane.lookAt(camera.position);
       }
    }
}

function onDocumentMouseUp(event) {
    controls.enabled = true;
    selectionMesh = false;
    selectionSDF = false;
}

function getMouse( e ) {
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;  
}
 
</script>
</html>
1 Like