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.
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>