There
SDFs in the scene - raymarching - #30 by hofk
I have a basic variant for moving an SDF via raycast.
Now I have combined raycasting for meshes and SDFs by creating two raycasters. I can move the meshes by clicking on them with the mouse on the vertical auxiliary plane.
However, I have problems with the SDF. It does not behave in the same way as the meshes. I have tried a number of variations, sometimes I get a properly moved SDF but the meshes don’t react or the other way around.
Maybe the approach with two separate raycasters doesn’t make sense? But how do you differentiate between mesh and SDF?
The Code
<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355 -->
<html>
<head>
<title>07_SDF_ShaderSoloV4</title>
<meta charset="utf-8" />
<style>
body{
overflow: hidden;
margin: 0;
}
</style>
</head>
<body></body>
<script type="module">
// @author hofk
import * as THREE from "../../jsm/three.module.173.js";
import { OrbitControls } from "../../jsm/OrbitControls.173.js";
document.addEventListener('mousedown', onDocumentMouseDown, false);
document.addEventListener('mousemove', onDocumentMouseMove, false);
document.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(20, 10, 80);
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 offsetSDF = 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( 40, 40, 40, 40 );
const auxPlaneMat = new THREE.MeshBasicMaterial({color: 0xaaaaaa, transparent:true, opacity:0.3, 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 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 GetDist(vec3 p) {
distCol dc;
distCol dcSphere;
vec3 pMouse = translateXYZ(p, mousePosition);
dcSphere.d = sdSphere( pMouse, boundingRadius);
dcSphere.c = vec4(1.0, 0.0, 0.0, 1.0);
dc = dcSphere; // apply to reserved dc
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 = [ 50.0, 50.0, 50.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]);
boxGeo.translate( boxParam[3], boxParam[4], boxParam[5]);
//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 },
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;
renderer.render(scene, camera);
}
function onDocumentMouseDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const vector = new THREE.Vector3(mouse.x, mouse.y, 1);
vector.unproject(camera);
raycasterMesh.set(camera.position, vector.sub(camera.position).normalize());
const intersects = raycasterMesh.intersectObjects(objectsToRaycast);
if (intersects.length > 0) {
controls.enabled = false;
selectionMesh = intersects[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 = raycasterMesh.intersectObject(auxPlane);
if (planeIntersects.length > 0) {
offsetSDF.copy(planeIntersects[0].point).sub(auxPlane.position);
}
}
}
}
function onDocumentMouseMove(event) {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
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 intersects = raycasterMesh.intersectObject(auxPlane);
if (intersects.length > 0) {
let newPos = intersects[0].point.clone().sub(offsetMesh);
selectionMesh.position.copy(newPos);
}
} else if (selectionSDF) {
const intersects = raycasterMesh.intersectObject(auxPlane);
if (intersects.length > 0) {
let newPos = intersects[0].point.clone().sub(offsetSDF);
let sdfPos = newPos.clone();
box.worldToLocal(sdfPos);
box.material.uniforms.mousePosition.value.copy(sdfPos);
}
} else {
const intersects = raycasterMesh.intersectObjects(objectsToRaycast);
if (intersects.length > 0) {
auxPlane.position.copy(intersects[0].object.position);
auxPlane.lookAt(camera.position);
}
}
}
function onDocumentMouseUp(event) {
controls.enabled = true;
selectionMesh = false;
selectionSDF = false;
}
</script>
</html>