How do you integrate Signed Distance Fields (SDF) into a basic three.js scene?

TIL gl_DepthRange. Wow! How long has this been available?

Hurray! it works. :slightly_smiling_face:

Link overwritten with improved file.


Annotation:

If I see it correctly, the last value for gl_FragColor is the transparency. In the current version r173 you can reduce the value 1.0 and get a brighter display, at 0 .0 it was then only white.

In the version up to r136 it doesn’t matter what number is there, it is obviously discarded.

There must have been an internal change in three.js ?

2 Likes

I have now generated a variant of the integration of the SDFs into the three.js scene, which I like quite a bit.

06_SDF-Shader-Raymarching

But there is one strange thing when you call up or reload the page. The position of the SDFs is shifted and a piece is a bit off to the side. If you move the scene briefly (OrbitControls), the SDF jumps to the correct position and it is perfect.

UPDATE:
found :upside_down_face:

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

then
renderer.setAnimationLoop( () => { renderer.render(scene, camera);});


The code:

<!DOCTYPE html>
 
<head>
  <title>06_SDF-Shader-Raymarching</title>
  <meta charset="utf-8" />
<style>
	body{
	overflow: hidden;
	margin: 0;
	}  
 </style>
</head>

<body></body>

<script type="module">

import * as THREE from "../jsm/three.module.173.js";
import { OrbitControls } from "../jsm/OrbitControls.173.js";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 65, window.innerWidth / window.innerHeight, 0.1, 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(0, 2, 4);
//controls
const controls = new OrbitControls(camera, renderer.domElement);
//controls.zoomSpeed = 2;
//controls.maxPolarAngle = Math.PI * 0.5;
//controls.update();

const axesHelper = new THREE.AxesHelper( 10 );
scene.add( axesHelper );
const gridHelper = new THREE.GridHelper( 10, 10 );
scene.add( gridHelper );

const sphMesh = new THREE.Mesh(new THREE.SphereGeometry(0.75, 12, 12), new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true}));
scene.add( sphMesh );
sphMesh.position.set( 1, 1, -1);

const define_SDFs = `
  float sdSphere( vec3 p, float radius ) {
    return length( p ) - radius;
  }
     
  float sdTorus( vec3 p, vec2 t ){
    return length( vec2(length(p.xz)-t.x,p.y) )-t.y;
  }
  
  float sdRoundBox( vec3 p, vec3 b, float r ){ // r  rounding
    vec3 q = abs(p) - b + r;
    return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
  }
 
  float sdBoxFrame( vec3 p, vec3 b, float e ){
      p = abs(p)-b;
      vec3 q = abs(p+e)-e;
      return min(min(
      length(max(vec3(p.x,q.y,q.z),0.0))+min(max(p.x,max(q.y,q.z)),0.0),
      length(max(vec3(q.x,p.y,q.z),0.0))+min(max(q.x,max(p.y,q.z)),0.0)),
      length(max(vec3(q.x,q.y,p.z),0.0))+min(max(q.x,max(q.y,p.z)),0.0));
  }`;

const designSDFs = `

 distCol GetDist(in vec3 p) {
  
  distCol dc;
  
  distCol dcSph;
  dcSph.d = sdSphere(p, 0.5 );
  dcSph.c = vec3( 0.6, 0.6, 0.1 );
  
  distCol dcTor;
  dcTor.d = sdTorus(p, vec2(0.575, 0.14));
  dcTor.c = vec3( 0.5, 0.1, 0.5 );
  
  //dc = opUnion(dcSph, dcTor);  // perform operations separately ?
  //.............................................
  float d = min(dcSph.d, dcTor.d);
  vec3  c = d < dcTor.d ? dcSph.c : dcTor.c; 
  //.............................................
  
  /*
  float dRBox = sdRoundBox(p, vec3(1.9, 0.05, 0.2), 0.02); // see boxGeometry definition: 2,2,2
  float dBoxFrm = sdBoxFrame(p, vec3(1.0, 0.1, 0.2), 0.02);
  
  float d = min(max(-dSph, dTor), dRBox);   // = SDF Union( SDF Subtraction )
  float d = min(max(-dSph, dTor), dBoxFrm);   // = SDF Union( SDF Subtraction )
  */

   dc.d = d;
   dc.c = c;
  return dc;    
}`
    
const vShader = `
  varying vec3 vPosition;
  varying vec2 vUv;
  void main() {
  	vPosition = position;
  	vUv = uv;
  	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`;

const fShader = `
  uniform vec3 camPos;
  varying vec3 vPosition;
  varying vec2 vUv;
  #define MAX_STEPS 250
  #define MAX_DIST 100.
  #define SURF_DIST 1e-4
  #define PI 3.1415926
  
  struct distCol { // distance, color
    float d;
    vec3 c;
  };
  
  mat2 Rot(float a) {
  float s = sin(a), c = cos(a);
  return mat2(c, -s, s, c);
  }
  vec2 opMin(vec2 a, vec2 b){
    return a.x < b.x ? a : b;
  } 
  ` 
  +
    define_SDFs
  +
    designSDFs
  +
  `
  distCol RayMarch(vec3 ro, vec3 rd) {
  
    distCol dc;
    float dO = 0.;
  	for(int i = 0; i < MAX_STEPS; i++) {
        vec3 p = ro + rd*dO;
        dc = GetDist(p);
        dO += dc.d;
  		if(dO > MAX_DIST || dO < SURF_DIST) break;
  	}
    dc.d = dO;
  	return dc;
  }

  vec3 GetNormal(vec3 p) {
  	float d = GetDist(p).d;
  	vec2 e = vec2(SURF_DIST, 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.;
  	float sca = 1.;
  	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., 1.);
  	return dif;
  }

  void main() {
  	vec2 uv = vUv-.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 col;
        vec3 p = ro + rd * dc.d;
        vec3 lightPos = vec3(2, 16, 3);
        vec3 dir = vec3(GetLight(p, lightPos));
        vec3 indir = vec3(.051*GetAo(p, GetNormal(p)));
        
        col = 1.1*dc.c + 0.5*dir + 0.4*indir;
        //col = mix(vec3(0.870588235), col, 0.9); // #dedede  rgb(222,222,222)

        //gl_FragColor = vec4( col, 0. ); // works up to r136
        gl_FragColor = vec4( col, 1. );   // also 136 and later up to 173 ... use also 0.9  0.8 ...
    }    
  }
  `;

const boxGeometry = new THREE.BoxGeometry(2, 2, 2);
 
const shaderMaterial = new THREE.ShaderMaterial({
  uniforms: {camPos: {value: new THREE.Vector3().copy(camera.position)}},
  vertexShader: vShader,
  fragmentShader: fShader,
  side: THREE.DoubleSide,
  transparent: true
});
const cube = new THREE.Mesh(boxGeometry, shaderMaterial);
scene.add(cube);
cube.position.set( 2, 1, -1 );

let camPos = new THREE.Vector3();

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

renderer.setAnimationLoop( () => { renderer.render(scene, camera);});

</script>
</html>
1 Like

Any plans for animation of SDF objects?

Seems, the answer is in the description of the issue.

You set camera position without the casting it into object’s local space, when you instantiate material:

{camPos: {value: new THREE.Vector3().copy(camera.position)}

But, when you move the camera with the controls, now it it’s in object’s local space:

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

The SDFs are in a box. I can therefore move them as a whole. Let’s see what works.

See SDFs in the scene - raymarching - #29 by hofk

1 Like

You’re so fast, I could have waited. When I posted it, I noticed it myself - but thank you!!!

1 Like

I’ve added primitive combinations.

07_SDF-Shader-Raymarching


 distCol opUnion(distCol dc1, distCol dc2) { 
  distCol dc;
  float d = min(dc1.d, dc2.d);
  vec3  c = d < dc2.d ? dc1.c : dc2.c; 
  dc.d = d;
  dc.c = c;     
  return dc;
}

distCol opSubtraction(distCol dc1, distCol dc2) { 
  distCol dc;
  float d = max(-dc1.d, dc2.d);
  vec3  c = d > dc2.d ? dc1.c : dc2.c; 
  dc.d = d;
  dc.c = c;     
  return dc;
}
  dc = opSubtraction(dcSph, dcTor);
  dc = opUnion(dc, dcRBox );


UPDATE
SDF and mesh appear fully integrated .

5 Likes

So far, something like this is already working in the test example.


  vec3 pTransSph = p - vec3( 0.5*cos(time),  0.2, 0.3*sin(time)); // translate  
  distCol dcSph;
  dcSph.d = sdSphere( pTransSph , 0.5 );
  dcSph.c = vec3( 0.6, 0.6, 0.1 );
  
  vec3 pTransTor = p - vec3( 0.4, -0.1, -0.3);
  vec3 pRotTor = rotateX(pTransTor, -0.3*time*PI);  // radiant ( -angle)
  distCol dcTor;
  dcTor.d = sdTorus(pRotTor, vec2(0.575, 0.14));
  dcTor.c = vec3( 0.5, 0.1, 0.5 );
  
  distCol dcRBox;
  dcRBox.d = sdRoundBox(p, vec3(0.8, 0.1, 0.2), 0.04);
  dcRBox.c = vec3( 0.1, 0.2, 1.0 );

With movement of the box containing the SDF:

4 Likes