SDFs in the scene - raymarching

Some examples of the integration of SDFs in three.js can be found in the questions post
How do you integrate Signed Distance Fields (SDF) into a basic three.js scene?

For example

SignedDistanceFields (SDF)
SDF-ShaderMaterial
SDF-GeometryGenerator
https://codepen.io/prisoner849/full/vEYYPwz
02_Raymarching
06_SDF-Shader-Raymarching


I have created a small application on this basis.

=> GitHub - hofk/threejsResources: Resources for three.js /_SDF_Shader

In the file SDF_designs.js
https://hofk.de/main/threejs/_SDF_Shader/00_SDF_Shader/SDF_designs.js
you develop your designs. There is a short guide at the beginning.

An example:

// make another design : index 1
//***************************************
SDF_designs[ 1 ] = `   // don't change this line
 
  distCol dcSph;
  dcSph.d = sdSphere(p, 0.25 );
  dcSph.c = vec4( 0.9, 0.8, 0.2, 1.0 );
  
  distCol dcTor;
  vec3 dcTorRotX = rotateX( p, 0.2*PI * time );
  dcTor.d = sdTorus(dcTorRotX, vec2(0.28, 0.08));
  dcTor.c = vec4( 0.4, 0.2, 0.7, 1.0 );
  
  dc = opUnion(dcSph, dcTor);

`;  // don't change this line
// .......................................................

I use a structure to take distance and color into account.

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

The primitives from Inigo Quilez are used. I have saved these in the file define_3D_SDFs.js
(supplemented by a custom SDFs from combinations).
https://hofk.de/main/threejs/_SDF_Shader/BasicFiles/define_3D_SDFs.js

The operations from the SDF_operations.js file are available for combining the primitives.
https://hofk.de/main/threejs/_SDF_Shader/BasicFiles/SDF_operations.js
As they take into account not only the distance but also the color, I have adapted them accordingly.
The file also contains translations and rotations for SDFs.

The shader part is outsourced to the file shaderParts.js to keep the html file short and clear.
https://hofk.de/main/threejs/_SDF_Shader/BasicFiles/shaderParts.js

In the actual application
:slightly_smiling_face: 00_SDF_Shader <<== Give it a try :slightly_smiling_face:

you only specify the size and position parameters for the boxes that are used to place the SDFs:

//   define size and position of boxes containing the SDFs from  SDF_designs.js
// *****************************************************************************
//               size, pos x, pos y,  pos z
boxParam[ 0 ] = [ 4.5,   4.0,   1.2,  -1.0 ];
boxParam[ 1 ] = [ 2.5,  -1.0,   2.1,   2.0 ];
boxParam[ 2 ] = [ 3.0,   0.0,   2.0,  -3.0 ];

// *****************************************************************************

To check the values, you can vary the following line:

transparent: true, // use false to see the boxes

The three examples are for demonstration purposes only and do not claim to be works of art. I am concerned with the structure and the program-technical procedure, I leave the creativity to interested users.

Perhaps someone will post their work of art here. The head can be the beginning and it can end like this: Shader - Shadertoy BETA.

Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more can serve as a small reference.

Sources https://iquilezles.org/


Addition:

JavaScript raymarching, Canvas Renderer

To familiarize myself with raymarching and rendering, I reprogrammed the demonstration example from the German video https://www.youtube.com/watch?v=rZ96YYUghpc (which is based purely on JavaScript) with some extensions for the positioning of SDFs, camera and light as well as variable shadows.

SDF-JS-Renderer

If you are not familiar with shaders, you can get an insight into pure Javascript here.
Please be patient, rendering in the CPU takes more time.

8 Likes

This is so cool. Really great addition to the toolbox.

How hard would it be to port something like:

I think you need Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more ( at the bottom )


Infinite and limited Repetition

Domain repetition is a very useful operator, since it allows you to create infinitely many primitives with a single object evaluation:

float opRepetition( in vec3 p, in vec3 s, in sdf3d primitive )
{
    vec3 q = p - s*round(p/s);
    return primitive( q );
}

and possibly more.

I don’t have all the operations in the SDF_operations.js file. But you can add as many as you like.

1 Like

Yeah… I’m thinking more because… you’ve set up the formal raymarching infra with camera position/ray direction etc.
This will make it a lot easier to port these kinds of raymarched effects. Very cool stuff!

2 Likes

This is great, thanks so much for sharing and arranging all the files in a clear and structured format. The previous post was really inspiring, I am pinning this also as I see a lot of room for expanding further into wider possibilities.

This sole line makes me wonder ÂżHave you considered converting this to a dedicated repo (i.e. github project)? I would love to contribute and this would make it easier to channel discussions. It is probably demanding to mantain, though

1 Like

I am on Github

and will post it there.

If there is time, I will also add a few things.
However, I am not able to moderate my repos in a timely manner.

3 Likes

Added to Github, see original post.

=> GitHub - hofk/threejsResources: Resources for three.js /_SDF_Shader

2 Likes

In order to organize further examples clearly and efficiently, I have renewed the folder structure.

2025-02-21 12.00.44
2025-02-21 12.02.21
2025-02-21 12.02.35

The links in the original post have already been adjusted.
The new structure can also be found on Github.

I have added operations
translateX, translateY, translateZ,
opElongate, opLimitedRepetition, opCheapBend, opDisplaceSin
and used them in example 01.
01_SDF_Shader

  distCol dcCappedTor;
  float angel =  0.9*PI; // radiant 0.0 .. 2.0*PI
  vec2 c = vec2(sin(0.5*angel),cos(0.5*angel));
  dcCappedTor.d = sdCappedTorus(translateY(rotateY(p, 0.7*PI), 0.8), c, 1.4, 0.3);
  dcCappedTor.c = vec4(0.9, 0.4, 0.2, 1.0);
  // ...
  distCol dcTor; 
  dcTor.d = sdTorus( translateZ(p, -0.8), vec2(0.8, 0.3)); 
  dcTor.c = vec4(0.1, 0.4, 0.6, 1.0);
  vec4 w = opElongate( translateZ(p, 1.9), vec3(0.1, 0.2, 0.8) );
  dcTor.d = min( dcTor.d, w.w + sdTorus(w.xyz, vec2(0.5, 0.2) ) );
  // ...
  distCol dcSphRep;
  float s = 1.01;
  vec3 n = vec3( 2.0, 3.0, 1.0);
  dcSphRep.d = sdSphere(opLimitedRepetition(rotateY(translateZ(p, -4.1), 0.25*PI), s, n), 0.5);
  dcSphRep.c = vec4(0.7, 0.8, 0.2, 1.0);
  // ...
  distCol dcBoxBend;
  dcBoxBend.d = sdBox( opCheapBend(translateX( p, -3.7), 0.07), vec3(3.0, 0.2, 0.5));
  dcBoxBend.c = vec4(0.2, 0.9, 0.3, 1.0);
  // ...
  distCol dcSphDispl;
  float displace = 1.5;
  float transX1 = 3.9;
  float transX2 = 4.4;
  vec3 dq1 = translateX(p, transX1);
  vec3 dq2 = translateX(p, transX2);
  dcSphDispl.d = sdSphere(dq1, 1.0) + opDisplaceSin(dq2, displace);
  dcSphDispl.c = vec4(0.9, 0.1, 0.9, 1.0);

  //---
  dc = opUnion(dcCappedTor, dcTor); // apply to reserved dc
  dc = opUnion(dc, dcSphRep);
  dc = opUnion(dc, dcBoxBend);
  dc = opUnion(dc, dcSphDispl);
1 Like

Now I have integrated the 2D SDFs
(see Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more )
and made some changes in the files shaderParts.js and SDF_operations.js.

The operations

// operations for 2D SDFs 
 
float opExtrusion(vec3 p, float sdf2D, float h)
{
    vec2 w = vec2( sdf2D, abs(p.z) - h );
  	return min(max(w.x,w.y),0.0) + length(max(w,0.0));
}

vec2 opRevolution( vec3 p, float w )
{
    return vec2( length(p.xz) - w, p.y );
}

enable the creation of 3D SDFs from 2D SDFs.

I have used these operations in example 2

02_SDF_Shader

  distCol dcHeartExtru;
  dcHeartExtru.d = opExtrusion(p, sdHeart(p.xy), 0.05 );
  dcHeartExtru.c = vec4(1.0, 0.1, 0.1, 1.0);
  
  // ...
  distCol dcCrossExtru;
  vec2 b = vec2(1.1, 0.25);
  vec3 q = translateX(p, 1.2);
  dcCrossExtru.d = opExtrusion(q, sdCross(q.xy, b, 0.2), 1.3 );
  dcCrossExtru.c = vec4(0.95, 0.9, 0.2, 1.0);
  
  //...
  distCol dcCrossRevolu;  
  dcCrossRevolu.d = sdCross(opRevolution(translateX(p, -2.8), 0.3), vec2(1.1, 0.25), 0.2);
  dcCrossRevolu.c = vec4(1.0, 0.4, 1.0, 1.0);
  
  // ...
  float tb = 0.33*PI;  // radiant
  vec2 sc = vec2(sin(tb),cos(tb));
  
  distCol dcArcExtru;
  dcArcExtru.d = opExtrusion(p, sdArc( p.xy, sc, 1.9, 0.2), 0.15 );   
  dcArcExtru.c = vec4(0.0, 0.1, 1.0, 1.0);
  // .
  distCol dcArcRevolu;
  dcArcRevolu.d = sdArc(opRevolution(rotateZ(translateY(p, -1.4), PI), 0.1), sc, 1.9, 0.1);
  dcArcRevolu.c = vec4(0.1, 1.0, 0.3, 1.0);
  
  //---
  dc = dcHeartExtru; // apply to reserved dc
  dc = opUnion( dc, dcCrossExtru);
  dc = opUnion( dc, dcCrossRevolu);
  dc = opUnion( dc, dcArcExtru);
  dc = opUnion( dc, dcArcRevolu);

All on Github.

4 Likes

Example 3 with movement in position and color.

For this I still have

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

in the operations.

Width, height and depth are now specified for the box instead of size.

03_SDF_Shader

3 Likes

Different coloring and SDFs in the transparent ball.

Used for the coloring, see SDF_operations.js

vec3 getNormal(vec3 p) {   // Note:   different from GetNormal in shaderParts.js
    vec2 e5 = vec2(1e-5, 0); // 
    float d1 = (p-e5.xyy).x;
    float d2 = (p-e5.yxy).y;
    float d3 = (p-e5.yyx).z;
    vec3 n = normalize(vec3(d1, d2, d3));
    return n;
}

New: operations for mirroring
dcVertCapsule.d = sdVerticalCapsule(mirrorXZ(rotateZ(translateX(p, 0.7), -0.74)), 1.2, 0.4);


vec3 mirrorYZ(vec3 p) {
    return vec3(-p.x, p.y, p.z);
}

vec3 mirrorXZ(vec3 p) {
    return vec3(p.x, -p.y, p.z);
}

vec3 mirrorXY(vec3 p) {
    return vec3(p.x, p.y, -p.z);
}

04_SDF_Shader

All on Github.

1 Like

Does it support web GPU renderer?

If you port all the stuff to TSL :thinking:

1 Like

In example 3, there is a very simple movement using sin/cos.

I now wanted to realize a movement like in my example FlightRouteQuaternion .

However, I was only partially successful. The movement on the space curve works, but the alignment of the capsule obviously does not. Also, the whole curve should appear as a yellow line (similar to Shader - Shadertoy BETA), but only a blinking point on the capsule is the result of my efforts. My knowledge and experience with shaders is still too poor.

05_SDF_Shader

To integrate the movement, I added the files quaternions.js and curveMovement.js to the BasicFiles and added them to shaderParts.js.

For the quaternions I use as in the source Shader - Shadertoy BETA @author jt
justified


struct quat
{
    float s;
    vec3 v;
};

The addition
quat setFromBasis( vec3 e1, vec3 e2, vec3 e3 ) // added @author hofk
is a modification of Quaternion - method .setFromBasis( e1, e2, e3 )

If you only want to achieve a fixed alignment with rotate(quat q, vec3 p), you can convert the values there, for example, and try them out visually: Quaternion - Axis, Angle Visualization

Splitting the fragment shader makes debugging more difficult. I have therefore created a version of the 5th example with a combined fragment shader. So far, however, this has not helped me. :roll_eyes:

05_SDF_ShaderSolo

The Code:

<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  --> 
<head>
  <title>05_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";
 
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, 0, 30);
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( 20 );
scene.add( axesHelper );

// shader
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 float time;
uniform vec3 camPos;
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;
};
// quaternion
struct quat
{
    float s;
    vec3 v;
};

quat mul(float s, quat q)
{
    return quat(s * q.s, s * q.v);
}

quat conjugate(quat q)
{
    return quat(q.s,-q.v);
}

float norm_squared(quat q)
{
    return q.s * q.s + dot(q.v, q.v);
}

quat div(quat q, float s)
{
    return quat(q.s / s, q.v / s);
}

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

quat invert(quat q) // NOTE: can't reuse function name inverse here
{
    return div(conjugate(q), norm_squared(q));
}

quat div(quat a, quat b)
{
    return mul(a, invert(b));
}

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
}

quat setFromBasis( vec3 e1, vec3 e2, vec3 e3 )  // added  @author hofk
{    
    float s;
    float x;
    float y;
    float z;
    float dis;
    float nd;
    float trace = e1.x + e2.y + e3.z;
    
    if ( trace > 0.0 ) {
        
        dis = trace + 1.0;
        dis = dis > 0.0 ? dis : 1e5; 
        nd = 0.5 / sqrt( dis );
        
        s = 0.25 / nd;
        x = -( e3.y - e2.z ) * nd;
        y = -( e1.z - e3.x ) * nd;
        z = -( e2.x - e1.y ) * nd;
        
    }
    else
    {
        if ( e1.x > e2.y && e1.x > e3.z ) {
            
            dis = 1.0 + e1.x - e2.y - e3.z;
            dis = dis > 0.0 ? dis : 1e5; 
            nd = 2.0 * sqrt( dis );
            
            s = ( e3.y - e2.z ) / nd;
            x = -0.25 * nd;
            y = -( e1.y + e2.x ) / nd;
            z = -( e1.z + e3.x ) / nd;
            
        }
        else
        {
            if ( e2.y > e3.z ) {
                
                dis = 1.0 + e2.y - e1.x - e3.z;
                dis = dis > 0.0 ? dis : 1e5;
                nd = 2.0 * sqrt( dis );
                
                s = ( e1.z - e3.x ) / nd;
                x = -( e1.y + e2.x ) / nd;
                y = -0.25 * nd;
                z = -( e2.z + e3.y ) / nd;
                
            } 
            else
            {
                dis = 1.0 + e3.z - e1.x - e2.y;
                dis = dis > 0.0 ? dis : 1e5;
                nd = 2.0 * sqrt( dis );
                
                s = ( e2.x - e1.y ) / nd;
                x = -( e1.z + e3.x ) / nd;
                y = -( e2.z + e3.y ) / nd;
                z = -0.25 * nd;
               
            }
            
        }
        
    }
    return quat(s, vec3(x, y, z));    
}

// Capsule
float sdVerticalCapsule(vec3 p, float h, float r) {
    p.y -= clamp(p.y, 0.0, h);
    return length(p) - r;
}

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

// curve movement
const int NUM_POINTS = 5;
int numPoints = NUM_POINTS;
float eps = 0.001;

vec3 controlPoints[NUM_POINTS] = vec3[](
    vec3( 10.0,  0.0,  0.0),
    vec3( 15.0,  5.0, -5.0),
    vec3(  5.0, 15.0,  0.0),
    vec3( -5.0, 10.0,  5.0),
    vec3(-10.0,  0.0,  5.0)
 );
 
vec3 catmullRom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) {
    float t2 = t * t;
    float t3 = t2 * t;
    return 0.5 * ((2.0 * p1) +
                (-p0 + p2) * t +
                (2.0*p0 - 5.0*p1 + 4.0*p2 - p3) * t2 +
                (-p0 + 3.0*p1 - 3.0*p2 + p3) * t3);
}

// Returns the point on the i-th segment, parameter t between 0 and 1
vec3 getCurvePoint(int i, float t, int numPoints ) {
    int i0 = (i - 1 + numPoints) % numPoints;
    int i1 = i;
    int i2 = (i + 1) % numPoints;
    int i3 = (i + 2) % numPoints;
    return catmullRom(controlPoints[i0], controlPoints[i1], controlPoints[i2], controlPoints[i3], t);
}

vec3 getMovingCapsulePos() {
    float u = mod(0.05*time, 1.0); // globalparameter 0 .. 1
    float segFloat = u * float(numPoints);
    int seg = int(floor(segFloat));
    float tCurve = fract(segFloat);
    return getCurvePoint(seg, tCurve, numPoints);
}

vec3 getTangent() {
    float u = mod(0.05*time, 1.0); // globalparameter 0 .. 1
    float segFloat = u * float(numPoints);
    int seg = int(floor(segFloat));
    float tCurve = fract(segFloat);
    return normalize(getCurvePoint(seg, tCurve + eps, numPoints) - getCurvePoint(seg, tCurve - eps, numPoints));
}

void getFrenetFrame( out vec3 tangent, out vec3 normal, out vec3 binormal) {
    tangent = getTangent();
    vec3 up = abs(tangent.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
    binormal = normalize(cross(tangent, up));
    normal = cross(binormal, tangent);
}


distCol GetDist( vec3 p ) { 
     distCol dc;
  // SDF design ..................................
    vec3 tangent, normal, binormal;
    getFrenetFrame(tangent, normal, binormal);
    quat qu = setFromBasis(tangent, normal, binormal); //  quaternion
  
    distCol dcVertCapsule;
    vec3 posOnCurve = getMovingCapsulePos();
    dcVertCapsule.d = sdVerticalCapsule( rotate(qu,translateXYZ(p, posOnCurve)), 2.0, 0.5 );
    dcVertCapsule.c = vec4(0.5, 0.5, 0.5, 1.0);
    
    dc = dcVertCapsule; // apply to reserved dc
  //..............................................	
    return dc;    
}

float getCurveDist(vec3 p) {
    float minDist = 100.0;
    const int numSamples = 50;
    for (int i = 0; i < numSamples; i++) {
        float uSample = float(i) / float(numSamples - 1); // u von 0 bis 1
        float segFloat = uSample * float(numPoints);
        int seg = int(floor(segFloat));
        float tCurve = fract(segFloat);
        vec3 pos = getCurvePoint(seg, tCurve, numPoints);
        float d = length(p - pos);
        minDist = min(minDist, d);
    }
    return minDist;
} 
  
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 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;
    
        float curveDist = getCurveDist(p);
        float lineThickness = 0.2;
        float curveOverlay = smoothstep(lineThickness, 0.0, curveDist);
        vec3 overlayColor = vec3(1.0, 1.0, 0.0); 
        vec3 finalColor = mix(sceneColor, overlayColor, curveOverlay);
        // vec3 finalColor = sceneColor + overlayColor * curveOverlay;
        gl_FragColor = vec4(finalColor, ct.a);     
    
    }
}`;
 
//   define size and position of box containing the SDFs 
// *****************************************************************************
//                   width, height, depth, pos x, pos y, pos z
const boxParam  = [ 1000.0, 1000.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 ]); //size

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: { camPos: {value: new THREE.Vector3().copy(camera.position)}, time: { value: 0.0 } },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: true,  // use false to see the boxes
}) ;
box = new THREE.Mesh(boxGeo, shaderMaterial);  // box with SDFs
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; // move SDF
    renderer.render( scene, camera );
} 
</script>
</html>

Is there a way to approach this thinking in a way that Minecraft processes “chunks” of space? Eg… “iterating through each block within a 16x16x16 block area (a “chunk”) and calculating the local sdf result” as if the sdf “passes through” a 3D “ethereal grid array” sdh :thinking:

I don’t understand exactly how this should look in an example.

Can you make a sketch with comments?

With a little help from AI, I can now display the curve.
However, I had imagined the result to be different.

I have hidden the alignment of the capsule using quaternions for the time being. It is not working properly yet.
05_SDF_ShaderSolo V1

The Code:


 <!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  --> 
<html>
<head>
  <title>05_SDF_ShaderSolo V1</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(0, 0, 30);

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

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

// quaternion
struct quat {
    float s;
    vec3 v;
};

quat mul(float s, quat q) {
    return quat(s * q.s, s * q.v);
}

quat conjugate(quat q) {
    return quat(q.s, -q.v);
}

float norm_squared(quat q) {
    return q.s * q.s + dot(q.v, q.v);
}

quat div(quat q, float s) {
    return quat(q.s / s, q.v / s);
}

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

quat invert(quat q) {
    return div(conjugate(q), norm_squared(q));
}

quat div(quat a, quat b) {
    return mul(a, invert(b));
}

vec3 rotate(quat q, vec3 p) {
    return mul(mul(q, quat(0.0, p)), invert(q)).v;
}

quat setFromBasis(vec3 e1, vec3 e2, vec3 e3) {
    float s;
    float x;
    float y;
    float z;
    float dis;
    float nd;
    float trace = e1.x + e2.y + e3.z;
    
    if(trace > 0.0) {
        dis = trace + 1.0;
        dis = dis > 0.0 ? dis : 1e5;
        nd = 0.5 / sqrt(dis);
        
        s = 0.25 / nd;
        x = -(e3.y - e2.z) * nd;
        y = -(e1.z - e3.x) * nd;
        z = -(e2.x - e1.y) * nd;
    } else {
        if(e1.x > e2.y && e1.x > e3.z) {
            dis = 1.0 + e1.x - e2.y - e3.z;
            dis = dis > 0.0 ? dis : 1e5;
            nd = 2.0 * sqrt(dis);
            
            s = (e3.y - e2.z) / nd;
            x = -0.25 * nd;
            y = -(e1.y + e2.x) / nd;
            z = -(e1.z + e3.x) / nd;
        } else {
            if(e2.y > e3.z) {
                dis = 1.0 + e2.y - e1.x - e3.z;
                dis = dis > 0.0 ? dis : 1e5;
                nd = 2.0 * sqrt(dis);
                
                s = (e1.z - e3.x) / nd;
                x = -(e1.y + e2.x) / nd;
                y = -0.25 * nd;
                z = -(e2.z + e3.y) / nd;
            } else {
                dis = 1.0 + e3.z - e1.x - e2.y;
                dis = dis > 0.0 ? dis : 1e5;
                nd = 2.0 * sqrt(dis);
                
                s = (e2.x - e1.y) / nd;
                x = -(e1.z + e3.x) / nd;
                y = -(e2.z + e3.y) / nd;
                z = -0.25 * nd;
            }
        }
    }
    return quat(s, vec3(x, y, z));
}

float sdVerticalCapsule(vec3 p, float h, float r) {
    p.y -= clamp(p.y, 0.0, h);
    return length(p) - r;
}

float dot2( in vec3 v ) { return dot(v,v); }
float udQuad( vec3 p, vec3 a, vec3 b, vec3 c, vec3 d )
{
  vec3 ba = b - a; vec3 pa = p - a;
  vec3 cb = c - b; vec3 pb = p - b;
  vec3 dc = d - c; vec3 pc = p - c;
  vec3 ad = a - d; vec3 pd = p - d;
  vec3 nor = cross( ba, ad );

  return sqrt(
    (sign(dot(cross(ba,nor),pa)) +
     sign(dot(cross(cb,nor),pb)) +
     sign(dot(cross(dc,nor),pc)) +
     sign(dot(cross(ad,nor),pd))<3.0)
     ?
     min( min( min(
     dot2(ba*clamp(dot(ba,pa)/dot2(ba),0.0,1.0)-pa),
     dot2(cb*clamp(dot(cb,pb)/dot2(cb),0.0,1.0)-pb) ),
     dot2(dc*clamp(dot(dc,pc)/dot2(dc),0.0,1.0)-pc) ),
     dot2(ad*clamp(dot(ad,pd)/dot2(ad),0.0,1.0)-pd) )
     :
     dot(nor,pa)*dot(nor,pa)/dot2(nor) );
}
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;
}

// CatmullRomCurve
// ----------------------
const int NUM_POINTS = 5;
int numPoints = NUM_POINTS;
float eps = 0.001;

vec3 controlPoints[NUM_POINTS] = vec3[](
    vec3( 10.0,  0.0,  0.0),
    vec3( 15.0,  5.0,  8.0),
    vec3(  5.0, 15.0,  0.0),
    vec3( -5.0, 10.0,  5.0),
    vec3(-10.0,  0.0,  5.0)
);
 
vec3 catmullRom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) {
    float t2 = t * t;
    float t3 = t2 * t;
    return 0.5 * ((2.0 * p1) +
                  (-p0 + p2) * t +
                  (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
                  (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
 
vec3 getCurvePoint(int i, float t, int numPoints) {
    int i0 = (i - 1 + numPoints) % numPoints;
    int i1 = i;
    int i2 = (i + 1) % numPoints;
    int i3 = (i + 2) % numPoints;
    return catmullRom(controlPoints[i0], controlPoints[i1], controlPoints[i2], controlPoints[i3], t);
}

vec3 getMovingCapsulePos() {
    float u = mod(0.05 * time, 1.0); // globaler Parameter 0 .. 1
    float segFloat = u * float(numPoints);
    int seg = int(floor(segFloat));
    float tCurve = fract(segFloat);
    return getCurvePoint(seg, tCurve, numPoints);
}

vec3 getTangent() {
    float u = mod(0.05 * time, 1.0);
    float segFloat = u * float(numPoints);
    int seg = int(floor(segFloat));
    float tCurve = fract(segFloat);
    return normalize(getCurvePoint(seg, tCurve + eps, numPoints) - getCurvePoint(seg, tCurve - eps, numPoints));
}

void getFrenetFrame(out vec3 tangent, out vec3 normal, out vec3 binormal) {
    tangent = getTangent();
    vec3 up = abs(tangent.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
    binormal = normalize(cross(tangent, up));
    normal = cross(binormal, tangent);
}

vec2 projectToScreen(vec3 worldPos) {
    vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(worldPos, 1.0);
    vec3 ndc = clipPos.xyz / clipPos.w;
    return ndc.xy * 0.5 + 0.5;
}

float getCurveScreenDist() {
    float minDist = 100.0;
    const int numSamples = 50;
    vec2 fragCoordNormalized = gl_FragCoord.xy / resolution;
    for (int i = 0; i < numSamples; i++) {
        float uSample = float(i) / float(numSamples - 1); // u  0 .. 1
        float segFloat = uSample * float(numPoints);
        int seg = int(floor(segFloat));
        float tCurve = fract(segFloat);
        vec3 posWorld = getCurvePoint(seg, tCurve, numPoints);
        vec2 posScreen = projectToScreen(posWorld);
        float d = distance(fragCoordNormalized, posScreen);
        minDist = min(minDist, d);
    }
    return minDist;
}

distCol GetDist(vec3 p) { 
    distCol dc;
    //vec3 tangent, normal, binormal;
    //getFrenetFrame(tangent, normal, binormal);
    //quat qu = setFromBasis(tangent, normal, binormal);
    
    distCol dcVertCapsule;
    vec3 posOnCurve = getMovingCapsulePos();
     
    //dcVertCapsule.d = sdVerticalCapsule(rotate(qu, translateXYZ(p, posOnCurve)), 2.0, 0.5);
    dcVertCapsule.d = sdVerticalCapsule( translateXYZ(p, posOnCurve), 2.0, 0.5);
    dcVertCapsule.c = vec4(0.5, 0.5, 0.5, 1.0);
    
    distCol dcQuad;
    dcQuad.d = udQuad(p, vec3(-1000.0, -1000.0, 0.0), vec3(500.0, -500.0, 0.0), vec3(500.0, 500.0, 0.0), vec3(-500.0, 500.0, 0.0));
    dcQuad.c= vec4(0.1, 0.2, 9.0, 1.0);
    
    dc = dcQuad;
    dc = opUnion( dc,dcVertCapsule);
    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 sceneColor = 0.7 * c + 0.5 * diff + 0.2 * ao;
    
        float curveScreenDist = getCurveScreenDist();
        float lineThickness = 0.004;
        float curveOverlay = smoothstep(lineThickness, 0.0, curveScreenDist);
        vec3 overlayColor = vec3(1.0, 1.0, 0.0);
        vec3 finalColor = mix(sceneColor, overlayColor, curveOverlay);
        gl_FragColor = vec4(finalColor, ct.a);
    }
}
`;

const boxParam = [ 1000.0, 1000.0, 1000.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]);

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: { 
        camPos: { value: new THREE.Vector3().copy(camera.position) },
        time: { value: 0.0 },
        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
    },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: false,
});
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>
1 Like

I can’t get the capsule to align with the tangent. The problem is that you can’t iterate over the curve like in the three.js solution to achieve a uniform change of the frenet frame.

Other attempts to circumvent the problem failed.

The following example shows what it looks like if the capsule is rotated around the axes in only three orthogonal directions.

05_SDF_ShaderSolo V2

I usually work with Firefox. Following a tip, I have tested other browsers.

Chrome Win 10 error
Opera Win 10 error
Samsung Galaxy, Android 14 Firefox and Chrome: Ok
Android 8 smartphone does not even show the three.js axes and almost hangs.

Bug in Chromium?

I just remembered that in my last project Single-branched geometry organically shaped in the web editor Firefox worked in some constellations and Chrome gave an error because angles were not calculated correctly. In the project, I use triangulation with massive internal angle calculations in the algorithm.

The Code:


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/sdfs-in-the-scene-raymarching/78355  --> 
<html>
<head>
  <title>05_SDF_ShaderSolo V2</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(0, 0, 30);

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

// 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 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 sdVerticalCapsule(vec3 p, float h, float r) {
    p.y -= clamp(p.y, 0.0, h);
    return length(p) - r;
}

float dot2( in vec3 v ) { return dot(v,v); }
float udQuad( vec3 p, vec3 a, vec3 b, vec3 c, vec3 d )
{
  vec3 ba = b - a; vec3 pa = p - a;
  vec3 cb = c - b; vec3 pb = p - b;
  vec3 dc = d - c; vec3 pc = p - c;
  vec3 ad = a - d; vec3 pd = p - d;
  vec3 nor = cross( ba, ad );

  return sqrt(
    (sign(dot(cross(ba,nor),pa)) +
     sign(dot(cross(cb,nor),pb)) +
     sign(dot(cross(dc,nor),pc)) +
     sign(dot(cross(ad,nor),pd))<3.0)
     ?
     min( min( min(
     dot2(ba*clamp(dot(ba,pa)/dot2(ba),0.0,1.0)-pa),
     dot2(cb*clamp(dot(cb,pb)/dot2(cb),0.0,1.0)-pb) ),
     dot2(dc*clamp(dot(dc,pc)/dot2(dc),0.0,1.0)-pc) ),
     dot2(ad*clamp(dot(ad,pd)/dot2(ad),0.0,1.0)-pd) )
     :
     dot(nor,pa)*dot(nor,pa)/dot2(nor) );
}

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

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

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 translateXYZ(vec3 p, vec3 q) {
    return p - q;
}

// CatmullRomCurve
// ----------------------
const int NUM_POINTS = 5;
int numPoints = NUM_POINTS;
float eps = 0.001;

vec3 controlPoints[NUM_POINTS] = vec3[](
    vec3( 10.0,  0.0,  0.0),
    vec3( 15.0,  5.0,  8.0),
    vec3(  5.0, 15.0,  0.0),
    vec3( -5.0, 10.0,  5.0),
    vec3(-10.0,  0.0,  5.0)
);
 
vec3 catmullRom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) {
    float t2 = t * t;
    float t3 = t2 * t;
    return 0.5 * ((2.0 * p1) +
                  (-p0 + p2) * t +
                  (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
                  (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
 
vec3 getCurvePoint(int i, float t, int numPoints) {
    int i0 = (i - 1 + numPoints) % numPoints;
    int i1 = i;
    int i2 = (i + 1) % numPoints;
    int i3 = (i + 2) % numPoints;
    return catmullRom(controlPoints[i0], controlPoints[i1], controlPoints[i2], controlPoints[i3], t);
}

vec3 getMovingCapsulePos() {
    float u = mod(0.05 * time, 1.0); // globaler Parameter 0 .. 1
    float segFloat = u * float(numPoints);
    int seg = int(floor(segFloat));
    float tCurve = fract(segFloat);
    return getCurvePoint(seg, tCurve, numPoints);
}

vec2 projectToScreen(vec3 worldPos) {
    vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(worldPos, 1.0);
    vec3 ndc = clipPos.xyz / clipPos.w;
    return ndc.xy * 0.5 + 0.5;
}

float getCurveScreenDist() {
    float minDist = 100.0;
    const int numSamples = 50;
    vec2 fragCoordNormalized = gl_FragCoord.xy / resolution;
    for (int i = 0; i < numSamples; i++) {
        float uSample = float(i) / float(numSamples - 1); // u  0 .. 1
        float segFloat = uSample * float(numPoints);
        int seg = int(floor(segFloat));
        float tCurve = fract(segFloat);
        vec3 posWorld = getCurvePoint(seg, tCurve, numPoints);
        vec2 posScreen = projectToScreen(posWorld);
        float d = distance(fragCoordNormalized, posScreen);
        minDist = min(minDist, d);
    }
    return minDist;
}

distCol GetDist(vec3 p) { 
    distCol dc;
 
    
    distCol dcVertCapsuleX;
    distCol dcVertCapsuleY;
    distCol dcVertCapsuleZ;
    vec3 posOnCurve = getMovingCapsulePos();
     
    dcVertCapsuleX.d = sdVerticalCapsule(rotateZ(translateXYZ(p, posOnCurve), 0.5*PI), 5.0, 0.1);
    dcVertCapsuleY.d = sdVerticalCapsule(translateXYZ(p, posOnCurve), 5.0, 0.1);
    dcVertCapsuleZ.d = sdVerticalCapsule(rotateX(translateXYZ(p, posOnCurve), -0.5*PI), 5.0, 0.1);
   
    dcVertCapsuleX.c = vec4(1.0, 0.2, 0.2, 1.0);
    dcVertCapsuleY.c = vec4(0.2, 1.0, 0.2, 1.0);
    dcVertCapsuleZ.c = vec4(0.2, 0.2, 1.0, 1.0);
    
    distCol dcQuad;
    dcQuad.d = udQuad(p, vec3(-1000.0, -1000.0, 0.0), vec3(500.0, -500.0, 0.0), vec3(500.0, 500.0, 0.0), vec3(-500.0, 500.0, 0.0));
    dcQuad.c= vec4(0.9, 0.2, 0.9, 1.0);
    
    dc = dcQuad;
    dc = opUnion(dc,dcVertCapsuleX);
    dc = opUnion(dc,dcVertCapsuleY);
    dc = opUnion(dc,dcVertCapsuleZ);
    
    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 sceneColor = 0.7 * c + 0.5 * diff + 0.2 * ao;
    
        float curveScreenDist = getCurveScreenDist();
        float lineThickness = 0.004;
        float curveOverlay = smoothstep(lineThickness, 0.0, curveScreenDist);
        vec3 overlayColor = vec3(1.0, 1.0, 0.0);
        vec3 finalColor = mix(sceneColor, overlayColor, curveOverlay);
        gl_FragColor = vec4(finalColor, ct.a);
    }
}
`;

const boxParam = [ 1000.0, 1000.0, 1000.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]);

shaderMaterial = new THREE.ShaderMaterial({
    uniforms: { 
        camPos: { value: new THREE.Vector3().copy(camera.position) },
        time: { value: 0.0 },
        resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
    },
    vertexShader: vShader,
    fragmentShader: fShader,
    side: THREE.DoubleSide,
    transparent: false,
});
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>

Then, to pass curve’s data (positions, normals, binormals and tangents) in DataTexture via a uniform may be an option? :thinking: