SDFs in the scene - raymarching

With little experience with shaders, I thought that all things could be done efficiently in the shader. But there are obviously limits.

So I would have to do my curve calculation like in the example FlightRouteQuaternion and bring the results into the shader?

You have published some examples with DataTexture. Can you provide one that is as simple and clear as possible to learn from?

Successfully sampled the spline data (positions, so far) from DataTexture.
The sphere is drawn on the box (the wireframed one)

I’ll try to create something with tangents, normals and binormals, and will make another post later :slight_smile:

4 Likes

I don’t even have time to look at how DataTexture works in principle as quickly as you create something like this. :+1:


I found this in the collection of yours.

eXtended eXamples 2020

It’s quite complicated, but basically does what I need it to do.

Yes, that Koi fish moves with pretty much the same approach.

As I promised, here is an example with tangents, normals and binormals, sampled from DataTexture.
See getSplineData() method, that’s how I instantiate the spline and write its data into DataTexture.
Sampling of the data is in lines 78-81 (JS section).

Video:


Demo: https://codepen.io/prisoner849/full/dPyvLqQ

3 Likes

I have simplified the example as much as possible to better understand the essentials.
SDF + Spline data (DataTexture)

Then I incorporated it into my structure. The only important thing is the change to THREE.ShaderMaterial.
ShaderMaterialDataTexture

The codes


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355/24  -->
<!-- adapted from   https://codepen.io/prisoner849/full/dPyvLqQ  -->
<head>
  <meta charset="UTF-8">
  <title>SDF + Spline data (DataTexture)</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body></body>
<script type="module">

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

const boxSize = 6;
const pointsCount = 5;

let sdf = {
pars_vertex:`
  varying vec3 vPosition;
  varying mat4 vProjMat;
  varying mat4 vModelViewMat;
  varying mat3 vNormalMatrix;
`,
vertex: `
  vPosition = position;
  vProjMat = projectionMatrix;
  vModelViewMat = modelViewMatrix;
  vNormalMatrix = normalMatrix;
`,
pars_fragment:`
  uniform float time;
  uniform vec3 camPos;

  varying vec3 vPosition;
  varying mat4 vProjMat;
  varying mat4 vModelViewMat;
  varying mat3 vNormalMatrix;
  
  #define MAX_STEPS 250
  #define MAX_DIST 100.
  #define SURF_DIST 1e-4
 
  vec2 GetDist(in vec3 p) {
      float t = time;
      // sample spline data
      float vStep = 1. / 4.;
      vec3 dataPos = vec3(texture(splineData, vec2(t * 0.1, vStep * 0.5)));
      vec3 dataTan = vec3(texture(splineData, vec2(t * 0.1, vStep * 1.5)));
      vec3 dataNor = vec3(texture(splineData, vec2(t * 0.1, vStep * 2.5)));
      vec3 dataBin = vec3(texture(splineData, vec2(t * 0.1, vStep * 3.5)));
      
      float axesLength = 1.0;
 
      float dF =  sdCapsule(p, dataPos, dataPos + dataTan * axesLength, 0.050);         // tangent
      dF = opUnion(dF, sdCapsule(p, dataPos, dataPos + dataNor * axesLength, 0.025));   // normal
      dF = opUnion(dF, sdCapsule(p, dataPos, dataPos + dataBin * axesLength, 0.012));   // binormal
                    
      vec2 d = vec2(dF, 1.); // the second parameter is a rudiment
      return d;
  }
      
  vec2 RayMarch(vec3 ro, vec3 rd) {
    vec2 dO=vec2(0.);
    for(int i = 0; i < MAX_STEPS; i++) {
      vec3 p = ro + rd*dO.x;
      vec2 dS = GetDist(p);
      dO.x += dS.x;
      dO.y = dS.y;
      if(dO.x>MAX_DIST || dO.x<SURF_DIST) break;
    }
    return dO;
  }

  vec3 GetNormal(vec3 p) {
    float d = GetDist(p).x;
    vec2 e = vec2(SURF_DIST, 0);

    vec3 n = d - vec3(
      GetDist(p-e.xyy).x,
      GetDist(p-e.yxy).x,
      GetDist(p-e.yyx).x);

    return normalize(n);
  }

`,

fragment: `
    vec3 ro = camPos;
    vec3 rd = normalize(vPosition - ro);
    vec2 d = RayMarch(ro, rd);
    vec3 col = vec3(1);

    vec3 p = ro + rd * d.x;
    vec3 sdfModelViewPosition = vec3(vModelViewMat * vec4(p, 1.));
    vec3 sdfNormal = normalize(vNormalMatrix * GetNormal(p));
`,

primitives: {
    capsule: `float sdCapsule( vec3 p, vec3 a, vec3 b, float r )
    {
        vec3 pa = p - a, ba = b - a;
        float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
        return length( pa - ba*h ) - r;
    }`
},

operations: `
  float opUnion( float d1, float d2 )
  {
      return min(d1,d2);
  }
` 
}
 
class SdfWSpline extends THREE.Mesh {
  constructor() {
    super();
    
    this.uniforms = {
      time: gu.time,
      camPos: { value: new THREE.Vector3() },
      splineData: { value: this.getSplineData() }
    };
    
    let m = new THREE.MeshPhongMaterial({
      color: 0xff4422, 
      transparent: true,
      side: THREE.DoubleSide,
      onBeforeCompile: (shader) => {
        shader.uniforms.time = this.uniforms.time;
        shader.uniforms.camPos = this.uniforms.camPos;
        shader.uniforms.splineData = this.uniforms.splineData;
        
        shader.vertexShader = `
          ${sdf.pars_vertex}
          ${shader.vertexShader}
        `.replace(
          `#include <begin_vertex>`,
          `#include <begin_vertex>
            ${sdf.vertex}
          `
        );
        
        shader.fragmentShader = `
          uniform sampler2D splineData;
          ${sdf.primitives.capsule}
          ${sdf.operations}
          ${sdf.pars_fragment}
          ${shader.fragmentShader}
          `
          .replace(
            `vec4 diffuseColor = vec4( diffuse, opacity );`,
            `
          ${sdf.fragment}
          
          vec4 diffuseColor = vec4( diffuse, opacity );
          diffuseColor.a = 1. - step(MAX_DIST - 1., d.x);
          
          vec4 dPos = vProjMat * vModelViewMat * vec4(p, 1.);
          gl_FragDepth = (dPos.z / dPos.w) / 2. + 0.5;
          `
          )
      }
    });
    
    let g = new THREE.BoxGeometry(boxSize, boxSize, boxSize);
    this.geometry = g;
    this.material = m;
    
    //helper
    this.add(
      new THREE.Box3Helper(
        new THREE.Box3().setFromBufferAttribute(g.attributes.position),
        0x888888
      )
    );
     
  } // end constructor
  
  getSplineData() {
    let spline = new THREE.CatmullRomCurve3(
      Array.from({ length: pointsCount }, () => {
        return new THREE.Vector3().randomDirection().setLength(0.5*boxSize);
      }),
      true
    );
    let segments = 1024;
    let positions = spline.getSpacedPoints(segments - 1);
    let splineTNB = spline.computeFrenetFrames(segments - 1, true);

    // spline helper
    this.add(
      new THREE.Points(
        new THREE.BufferGeometry().setFromPoints(spline.points),
        new THREE.PointsMaterial({ size: 0.15, color: 0xffff44 })
      )
    );
    
    this.add(
      new THREE.Line(
        new THREE.BufferGeometry().setFromPoints(positions),
        new THREE.LineBasicMaterial({ color: 0x0088ff })
      )
    );
    
    // datatexture
    let data = new Float32Array(segments * 4 * 4);
    let fillData = (idx, row, array) => {
      let startIdx = (idx + segments * row) * 4;
      data[startIdx + 0] = array[idx].x;
      data[startIdx + 1] = array[idx].y;
      data[startIdx + 2] = array[idx].z;
      data[startIdx + 3] = 0;
    };
    for (let i = 0; i < segments; i++) {
      fillData(i, 0, positions);
      fillData(i, 1, splineTNB.tangents);
      fillData(i, 2, splineTNB.normals);
      fillData(i, 3, splineTNB.binormals);
    }

    let dataTexture = new THREE.DataTexture(
      data,
      segments,
      4,
      THREE.RGBAFormat,
      THREE.FloatType
    );
    dataTexture.wrapS = THREE.RepeatWrapping;
    dataTexture.needsUpdate = true;

    return dataTexture;
    
  } //end getSplineData
  
} // end class SdfWSpline

let gu = { time: { value: 0  }};

let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(  45, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(4, 5, 10);
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

window.addEventListener("resize", (event) => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
 
scene.add( new THREE.AmbientLight(0xffffff, 2 ));
 
let sdfWSpline = new SdfWSpline();
scene.add(sdfWSpline);

let clock = new THREE.Clock();
let t = 0;

renderer.setAnimationLoop(() => {
  let dt = clock.getDelta();
  t += dt;
  gu.time.value = t;
  controls.update();

  sdfWSpline.worldToLocal(
    sdfWSpline.uniforms.camPos.value.copy(camera.position)
  );
  renderer.render(scene, camera);
});
</script>
</html>

ShaderMaterial


 <!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355/24  -->
<!-- adapted from   https://codepen.io/prisoner849/full/dPyvLqQ  -->
<!-- transferred to ShaderMaterial -->
<html>
<head>
  <title>ShaderMaterialDataTexture</title>
  <meta charset="utf-8">
  <style>
    body { margin:0; overflow:hidden; }
  </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(45, window.innerWidth/ window.innerHeight, 0.1, 1000);
camera.position.set(10, 10, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);

const pointsCount = 5;
const boxSize = 6;

const spline = new THREE.CatmullRomCurve3(
Array.from({ length: pointsCount }, () => new THREE.Vector3().randomDirection().setLength(boxSize * 0.5)),
true  // closed curve
);
const segments = 1024;
const positions = spline.getSpacedPoints(segments - 1);
const splineTNB = spline.computeFrenetFrames(segments - 1, true);

const splinePoints = new THREE.Points(
new THREE.BufferGeometry().setFromPoints(spline.points),
new THREE.PointsMaterial({ size: 0.15, color: 0xffff44 })
);
scene.add(splinePoints);
const splineLine = new THREE.Line(
new THREE.BufferGeometry().setFromPoints(positions),
new THREE.LineBasicMaterial({ color: 0xff44ff })
);
scene.add(splineLine);

// create DataTexture
const data = new Float32Array(segments * 4 * 4); // segments * rows * 4 components (RGBA)
function fillData(idx, row, array) {
    const start = (idx + segments * row) * 4;
    data[start + 0] = array[idx].x;
    data[start + 1] = array[idx].y;
    data[start + 2] = array[idx].z;
    data[start + 3] = 0.0;
}
for (let i = 0; i < segments; i++) {
    fillData(i, 0, positions);
    fillData(i, 1, splineTNB.tangents);
    fillData(i, 2, splineTNB.normals);
    fillData(i, 3, splineTNB.binormals);
}
const splineDataTexture = new THREE.DataTexture(
    data,
    segments,
    4,
    THREE.RGBAFormat,
    THREE.FloatType
);
splineDataTexture.wrapS = THREE.RepeatWrapping;
splineDataTexture.needsUpdate = true;

const geometry = new THREE.BoxGeometry(boxSize, boxSize, boxSize);

const vertexShader = `
varying vec3 vPosition;
varying vec2 vUv;

void main(){
  vPosition = position;
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
uniform float time;
uniform vec3 camPos;
uniform vec2 resolution;
uniform sampler2D splineData;

varying vec3 vPosition;
varying vec2 vUv;

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

float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
  vec3 pa = p - a;
  vec3 ba = b - a;
  float h = clamp(dot(pa,ba)/ dot(ba,ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}
 
float opUnion(float d1, float d2) {
  return min(d1, d2);
}

vec2 GetDist(in vec3 p) {
   
  float t = mod( time * 0.1, 1.0);
  float vStep = 1.0 / 4.0;
   
  vec3 dataPos = texture(splineData, vec2(t, vStep * 0.5)).rgb;
  vec3 dataTan = texture(splineData, vec2(t, vStep * 1.5)).rgb;
  vec3 dataNor = texture(splineData, vec2(t, vStep * 2.5)).rgb;
  vec3 dataBin = texture(splineData, vec2(t, vStep * 3.5)).rgb;
  
  float axesLength = 1.0;
   
  float dCapsule = sdCapsule(p, dataPos, dataPos + dataTan * axesLength, 0.05);
  dCapsule = opUnion(dCapsule, sdCapsule(p, dataPos, dataPos + dataNor * axesLength, 0.025));
  dCapsule = opUnion(dCapsule, sdCapsule(p, dataPos, dataPos + dataBin * axesLength, 0.012));
  
  return vec2(dCapsule, 1.0);
  
}

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

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

// Ambient Occlusion
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).x;
    occ += (h - d) * sca;
    sca *= 0.95;
  }
  return clamp(1.0 - 1.5 * occ, 0.0, 1.0);
}
// Light
float GetLight(vec3 p, vec3 lPos) {
  vec3 l = normalize(lPos - p);
  vec3 n = GetNormal(p);
  return clamp(dot(n, l), 0.0, 1.0);
}

void main(){
  vec3 ro = camPos;
  vec3 rd = normalize(vPosition - ro);
  vec2 d = RayMarch(ro, rd);
  if (d.x >= MAX_DIST) {
    gl_FragColor = vec4(0.0);
  } else {
    vec3 p = ro + rd * d.x;
    vec3 lightPos = vec3(2.0, 16.0, 3.0);
    float diff = GetLight(p, lightPos);
    float ao = 0.051 * GetAo(p, GetNormal(p));
    vec3 col = vec3(0.7) + diff * 0.5 + ao * 0.2;
     
    gl_FragColor = vec4(col, 1.0);
  }
}
`;

const material = new THREE.ShaderMaterial({
uniforms: {
  time: { value: 0.0 },
  camPos: { value: new THREE.Vector3().copy(camera.position) },
  resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
  splineData: { value: splineDataTexture }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
side: THREE.DoubleSide,
transparent: true
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

window.addEventListener("resize", () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
material.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
});

const clock = new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
material.uniforms.time.value = clock.getElapsedTime();
// Update camPos in local space ( box )
mesh.worldToLocal(material.uniforms.camPos.value.copy(camera.position));
controls.update();
renderer.render(scene, camera);
}
animate();

</script>
</html>
3 Likes

I have added DataTexture to the solo version of my project with the structure

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

incorporated.

05_SDF_ShaderSolo V4

From this variant, I can transfer it directly into the project with a separate definition of the design of the SDFs.

The Code:


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  -->
<!-- See the templates for DataTexture: 
        https://codepen.io/prisoner849/pen/dPyvLqQ 
        https://hofk.de/main/threejs/_SDF_Shader/sdf-spline-data-datatexture/dist/DataTexture.html
-->
<html>
<head>
  <title>05_SDF_ShaderSolo V4</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";

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(5, 5, 10);

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( 3 );
scene.add(axesHelper);

const pointsCount = 5;
const boxSize = 6;

// 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 vec3 camPos;
uniform vec2 resolution;
uniform sampler2D splineData;

varying vec3 vPosition;
varying vec2 vUv;

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

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

// ... SDF ........................................
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
  vec3 pa = p - a;
  vec3 ba = b - a;
  float h = clamp(dot(pa,ba)/ dot(ba,ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}

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

distCol GetDist(vec3 p) {
    float t = mod(time * 0.1, 1.0);
    float vStep = 1.0 / 4.0;
    
    vec3 dataPosition = texture(splineData, vec2(t, vStep * 0.5)).rgb;
    vec3 dataTangent  = texture(splineData, vec2(t, vStep * 1.5)).rgb;
    vec3 dataNormal   = texture(splineData, vec2(t, vStep * 2.5)).rgb;
    vec3 dataBinormal = texture(splineData, vec2(t, vStep * 3.5)).rgb;
    
    float axesLength = 0.6;
    
    //... SDF ...  
    distCol dcCapsuleTangent;
    dcCapsuleTangent.d = sdCapsule(p, dataPosition, dataPosition + dataTangent * axesLength, 0.05);
    dcCapsuleTangent.c =  vec4(1.0, 1.0, 0.2, 1.0);
    distCol dcCapsuleNormal;
    dcCapsuleNormal.d = sdCapsule(p, dataPosition, dataPosition + dataNormal * axesLength, 0.05);
    dcCapsuleNormal.c =  vec4(0.2, 1.0, 0.2, 1.0);
    distCol dcCapsuleBinormal;
    dcCapsuleBinormal.d = sdCapsule(p, dataPosition, dataPosition + dataBinormal * axesLength, 0.05);    
    dcCapsuleBinormal.c =  vec4(0.2, 0.8, 1.0, 1.0);
    
    distCol dc;  // apply to reserved dc
    dc = dcCapsuleTangent;
    dc = opUnion(dc, dcCapsuleNormal);
    dc = opUnion(dc, dcCapsuleBinormal);
    
    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); // 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 sceneColor = 0.7 * c + 0.5 * diff + 0.2 * ao;
        gl_FragColor = vec4(sceneColor, ct.a);
        
    }
}
`;

let spline = new THREE.CatmullRomCurve3(
  Array.from({ length: pointsCount }, () => {
    return new THREE.Vector3().randomDirection().setLength(0.5*boxSize);
  }),
  true // closed
);
let segments = 1024;
let positions = spline.getSpacedPoints(segments - 1);
let splineTNB = spline.computeFrenetFrames(segments - 1, true);

// spline helper
scene.add(
  new THREE.Points(
    new THREE.BufferGeometry().setFromPoints(spline.points),
    new THREE.PointsMaterial({ size: 0.15, color: 0xffff44 })
  )
);

scene.add(
  new THREE.Line(
    new THREE.BufferGeometry().setFromPoints(positions),
    new THREE.LineBasicMaterial({ color: 0xff44ff })
  )
);

// DataTexture: Position, Tangent, Normal, Binormal

const data = new Float32Array(segments * 4 * 4); // segments * rows * 4 components (RGBA)

function fillData(idx, row, array) {
    const start = (idx + segments * row) * 4;
    data[start + 0] = array[idx].x;
    data[start + 1] = array[idx].y;
    data[start + 2] = array[idx].z;
    data[start + 3] = 0.0;
}
for (let i = 0; i < segments; i++) {
    fillData(i, 0, positions);
    fillData(i, 1, splineTNB.tangents);
    fillData(i, 2, splineTNB.normals);
    fillData(i, 3, splineTNB.binormals);
}
const splineDataTexture = new THREE.DataTexture(
    data,
    segments,
    4,
    THREE.RGBAFormat,
    THREE.FloatType
);
splineDataTexture.wrapS = THREE.RepeatWrapping;
splineDataTexture.needsUpdate = true;

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

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

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

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

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: {
        time: { value: 0.0 },
        camPos: { value: new THREE.Vector3().copy(camera.position) },
        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
        splineData: { value: splineDataTexture }
    },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: true,
});
box = new THREE.Mesh(boxGeo, shaderMaterial);
scene.add(box);
box.position.set(boxParam[3], boxParam[4], boxParam[5]);

camPos = new THREE.Vector3();

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

const clock = new THREE.Clock();
let t = 0.0;

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

function animate() {
    t = clock.getElapsedTime(); 
    requestAnimationFrame(animate);
    shaderMaterial.uniforms.time.value = t;
    renderer.render(scene, camera);
}
</script>
</html>
3 Likes

Looks like there’s a lot of interest in this space currently :eyes:https://x.com/miketuritzin/status/1897768831793480049

Excuse the x link :neutral_face:

1 Like

Gltf model and SDF now move on a curve. The problem was that not all SDFs such as sdCapsule (e.g. sdVerticalCappedCylinder ) can be aligned with the TNB system using the start and end points. Therefore, quaternions have to be added.

What caused me problems is the different convention in JS/three.js and Shader.

It already works in the solo version, and the insertion into the project should now work.

06_SDF_ShaderSolo

TheCode



<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  -->
<!-- See the templates for DataTexture: 
        https://codepen.io/prisoner849/pen/dPyvLqQ 
        https://hofk.de/main/threejs/_SDF_Shader/sdf-spline-data-datatexture/dist/DataTexture.html
-->
<html>
<head>
  <title>06_SDF_ShaderSolo</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";
import { GLTFLoader } from "../../jsm/GLTFLoader.173.js";
THREE.Quaternion.prototype.setFromBasis = function( e1, e2, e3 ) {
    
    const    m11 = e1.x, m12 = e1.y, m13 = e1.z,
            m21 = e2.x, m22 = e2.y, m23 = e2.z,
            m31 = e3.x, m32 = e3.y, m33 = e3.z,
            trace = m11 + m22 + m33;

    if ( trace > 0 ) {

        const s = 0.5 / Math.sqrt( trace + 1.0 );

        this._w = 0.25 / s;
        this._x = -( m32 - m23 ) * s;
        this._y = -( m13 - m31 ) * s;
        this._z = -( m21 - m12 ) * s;

    } else if ( m11 > m22 && m11 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

        this._w = ( m32 - m23 ) / s;
        this._x = -0.25 * s;
        this._y = -( m12 + m21 ) / s;
        this._z = -( m13 + m31 ) / s;

    } else if ( m22 > m33 ) {

        const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

        this._w = ( m13 - m31 ) / s;
        this._x = -( m12 + m21 ) / s;
        this._y = -0.25 * s;
        this._z = -( m23 + m32 ) / s;

    } else {

        const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

        this._w = ( m21 - m12 ) / s;
        this._x = -( m13 + m31 ) / s;
        this._y = -( m23 + m32 ) / s;
        this._z = -0.25 * s;

    }
    
    this._onChangeCallback();

    return this;
 
}

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(3, 3, 7);

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( 3 );
scene.add(axesHelper);

const pointsCount = 5;
const boxSize = 6;

// 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 vec3 camPos;
uniform vec2 resolution;
uniform sampler2D splineData;

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;
};

struct quat
{
    float s;
    vec3 v;
};
quat conjugate(quat q)
{
    return quat(q.s,-q.v);
}
quat div(quat q, float s)
{
    return quat(q.s / s, q.v / s);
}
float norm_squared(quat q)
{
    return q.s * q.s + dot(q.v, q.v);
}
//quat inverse(quat q) // ERROR: "Name of a built-in function cannot be redeclared as function"
quat invert(quat q) // NOTE: can't reuse function name inverse here
{
    return div(conjugate(q), norm_squared(q));
}

quat mul(quat a, quat b)
{
    return quat(a.s * b.s - dot(a.v, b.v), a.s * b.v + b.s * a.v + cross(a.v, b.v));
}
 
vec3 rotate(quat q, vec3 p) // NOTE: order of parameters copies order of applying rotation matrix: M v
{
    return mul(mul(q, quat(0.0, p)), invert(q)).v; // NOTE: in case of unit-quaternion reciprocal can be replaced by conjugate
}
 
vec3 rotate(float angle, vec3 axis, vec3 point) // NOTE: axis must be unit!
{
    float c = cos(angle);
    float s = sin(angle);
    return c * point + s * cross(axis, point) + (1.0 - c) * (dot(point, axis) * axis); // Rodrigues' Rotation Formula
}

vec3 rotateZ(vec3 p, float angle) {
    float s = sin(angle);
    float c = cos(angle);
    return vec3(
        p.x * c - p.y * s,
        p.x * s + p.y * c,
        p.z
    );
}
vec3 rotateX(vec3 p, float angle) {
    float s = sin(angle);
    float c = cos(angle);
    return vec3(
        p.x,
        p.y * c - p.z * s,
        p.y * s + p.z * c
    );
}
 
// ... SDF ........................................

float sdVerticalCappedCylinder( vec3 p, float h, float r )
{
  vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(r,h);
  return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}
float sdCapsule( vec3 p, vec3 a, vec3 b, float r )
{
  vec3 pa = p - a, ba = b - a;
  float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
  return length( pa - ba*h ) - r;
}
distCol opUnion(distCol dc1, distCol dc2) { 
  distCol dc;
  float d = min(dc1.d, dc2.d);
  vec4 c = d < dc2.d ? dc1.c : dc2.c; 
  dc.d = d;
  dc.c = c;     
  return dc;
}

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

distCol GetDist(vec3 p) {
    float t = mod(time * 0.05, 1.0);
    float vStep = 1.0 / 5.0;
    
    vec3 dataPosition = texture(splineData, vec2(t, vStep * 0.5)).xyz;
    vec3 dataTangent  = texture(splineData, vec2(t, vStep * 1.5)).xyz;
    vec3 dataNormal   = texture(splineData, vec2(t, vStep * 2.5)).xyz;
    vec3 dataBinormal = texture(splineData, vec2(t, vStep * 3.5)).xyz;
    vec4 dataQuaternion = texture(splineData, vec2(t, vStep * 4.5)).xyzw;
    
    //... SDF ...
    quat qu;
    
    qu.s = dataQuaternion.w;
    qu.v = dataQuaternion.xyz;
    
    distCol dcCyl;
    dcCyl.d = sdVerticalCappedCylinder( rotate(invert(qu), translateXYZ(p, dataPosition)), 0.4, 0.04 );
    dcCyl.c =  vec4(1.0, 0.2, 0.2, 1.0);
    
    float axesLength = 0.6; 
    distCol dcCapsuleTangent;
    dcCapsuleTangent.d = sdCapsule(p, dataPosition, dataPosition + dataTangent * axesLength, 0.03);
    dcCapsuleTangent.c =  vec4(1.0, 1.0, 0.2, 1.0);
    distCol dcCapsuleNormal;
    dcCapsuleNormal.d = sdCapsule(p, dataPosition, dataPosition + dataNormal * axesLength, 0.03);
    dcCapsuleNormal.c =  vec4(0.2, 1.0, 0.2, 1.0);
    distCol dcCapsuleBinormal;
    dcCapsuleBinormal.d = sdCapsule(p, dataPosition, dataPosition + dataBinormal * axesLength, 0.03);    
    dcCapsuleBinormal.c =  vec4(0.2, 0.8, 1.0, 1.0);
    
    distCol dc;  // apply to reserved dc
    dc = dcCapsuleTangent;
    dc = opUnion(dc, dcCapsuleNormal);
    dc = opUnion(dc, dcCapsuleBinormal);
    
    dc = opUnion(dc, dcCyl );
    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); // 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 sceneColor = 0.7 * c + 0.5 * diff + 0.2 * ao;
        gl_FragColor = vec4(sceneColor, ct.a);
        
    }
}
`;

// movement

let spline = new THREE.CatmullRomCurve3(
  Array.from({ length: pointsCount }, () => {
    return new THREE.Vector3().randomDirection().setLength(0.5*boxSize);
  }),
  true // closed
);
let segments = 1024;
let positions = spline.getSpacedPoints(segments - 1);
let splineTNB = spline.computeFrenetFrames(segments - 1, true);

let splineQuats = []; // for movement of the gltf model
let shaderQuats = []; // for movement of SDFs in the shader
const offset = new THREE.Quaternion(-0.5, -0.5, -0.5, 0.5);

for ( let i = 0; i < segments; i ++ ) { // calculate quaternions from Basis TNB and BTN (different order in the shader)
    
   const quat = new THREE.Quaternion( ).setFromBasis( splineTNB.tangents[ i ], splineTNB.normals[ i ], splineTNB.binormals[ i ] ); 
   splineQuats.push( quat );
   
   const quatSh = quat.clone().multiply(offset); 
   shaderQuats.push( quatSh );
     
}
 
// spline helper
scene.add(
  new THREE.Points(
    new THREE.BufferGeometry().setFromPoints(spline.points),
    new THREE.PointsMaterial({ size: 0.15, color: 0xffff44 })
  )
);

scene.add(
  new THREE.Line(
    new THREE.BufferGeometry().setFromPoints(positions),
    new THREE.LineBasicMaterial({ color: 0xff44ff })
  )
);

// DataTexture: Position, Tangent, Normal, Binormal, Quaternion
const dataRows = 5;
const data = new Float32Array(segments * dataRows * 4); // segments * dataRows * 4 components (RGBA)

function fillData(idx, row, array) {
    const start = (idx + segments * row) * 4;
    data[start + 0] = array[idx].x;
    data[start + 1] = array[idx].y;
    data[start + 2] = array[idx].z;
    data[start + 3] = array[idx].w;

}
for (let i = 0; i < segments; i++) {
    fillData(i, 0, positions);
    fillData(i, 1, splineTNB.tangents);
    fillData(i, 2, splineTNB.normals);
    fillData(i, 3, splineTNB.binormals);
    fillData(i, 4, shaderQuats);
    
}
const splineDataTexture = new THREE.DataTexture(
    data,
    segments,
    dataRows,
    THREE.RGBAFormat,
    THREE.FloatType
);
splineDataTexture.wrapS = THREE.RepeatWrapping;
splineDataTexture.needsUpdate = true;

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

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

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

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

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: {
        time: { value: 0.0 },
        camPos: { value: new THREE.Vector3().copy(camera.position) },
        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
        splineData: { value: splineDataTexture }
    },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: true,
});
box = new THREE.Mesh(boxGeo, shaderMaterial);
scene.add(box);
box.position.set(boxParam[3], boxParam[4], boxParam[5]);

camPos = new THREE.Vector3();

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

const clock = new THREE.Clock();
let t = 0.0;

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

const loader = new GLTFLoader( );
const shuttle = new THREE.Object3D( );
loader.load( 'Space Shuttle/SpaceShuttle(1).gltf', processShuttle ); // (CC-BY) Poly by Googl
 
let iShuttle = 0;

animate();

function animate() {
    t = clock.getElapsedTime(); 
    requestAnimationFrame(animate);
    driving( );
    shaderMaterial.uniforms.time.value = 2.0*t;
    renderer.render(scene, camera);
}

function driving( ) {
    
   if ( iShuttle === segments ) {
        
        iShuttle = 0; // loop
 
    }
     
    shuttle.quaternion.set(splineQuats[ iShuttle ].x, splineQuats[ iShuttle ].y, splineQuats[ iShuttle ].z, splineQuats[ iShuttle ].w);
    shuttle.position.set( positions[ iShuttle ].x , positions[ iShuttle ].y, positions[ iShuttle ].z );
    
    iShuttle ++;
    
}

function processShuttle( gltf ) {
    
    gltf.scene.rotation.x = -Math.PI / 2; 
    gltf.scene.rotation.z = -Math.PI / 2; 
    shuttle.add( gltf.scene );
    shuttle.scale.set( 0.015, 0.015, 0.015 ); // because gltf.scene is big
    scene.add( shuttle );
    
}

</script>
</html>
2 Likes

The movement has now been added.

06_SDF_Shader

3 Likes

Raycasting also works fundamentally in the example

07_SDF_ShaderSolo

But I need to work it out a little more finely.

1 Like