The articles Image Texture as ShaderPass uniform and
Signed Distance Fields for fun and profit reminded me that I briefly looked into Signed Distance Fields (SDF) in 2018 after the articles Clipping Solids (SDF functions) and Clipping Solids (SDF) Modelling / Sculpting.
I abandoned it at the time because of other things. Now it has sparked my interest again.
In order to be able to experiment, I have tried to create a test environment with three.js that is as minimal as possible.
After some research ( also AI see AI - trustworthy or not? - #18 by hofk )
I have a small script: SignedDistanceFields (SDF)
However, the three.js scene is completely overlaid and OrbitControls, AxesHelper and other things are not effective.
I have not yet been able to find out whether this is even possible with the EffectComposer variant.
Does it make more sense to use ShaderMaterial as the basis?
My knowledge of shaders is more on a basic level, I have a link to this at Collection of examples from discourse.threejs.org
see ShaderBeginner.
Interesting links to SDF
Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more
Inigo Quilez :: computer graphics, mathematics, shaders, fractals, demoscene and more
Intro to Signed Distance Fields
https://www.youtube.com/watch?v=rZ96YYUghpc (only German)
The code
<!DOCTYPE html>
<!-- @author hofk -->
<head>
<meta charset="UTF-8" />
<title>SignedDistanceFields (SDF)</title>
</head>
<body></body>
<script type="module">
import * as THREE from "../jsm/three.module.173.js";
import { OrbitControls} from '../jsm/OrbitControls.173.js';
import { EffectComposer } from "../jsm/EffectComposer.173.js";
import { RenderPass } from "../jsm/RenderPass.173.js";
import { ShaderPass } from "../jsm/ShaderPass.173.js";
const WIDTH = 800;
const HIGHT = 800;
const scene = new THREE.Scene( );
const camera = new THREE.PerspectiveCamera( 65, WIDTH / HIGHT, 0.01, 1000);
camera.position.set( 1, 3, 12 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( WIDTH, HIGHT );
document.body.appendChild( renderer.domElement );
/*
const controls = new OrbitControls( camera, renderer.domElement );
const axesHelper = new THREE.AxesHelper( 10 );
scene.add( axesHelper );
*/
const customShader = {
uniforms: {
time: { value: 0.0 },
resolution: { value: new THREE.Vector2( WIDTH, HIGHT ) },
cameraPos: { value: camera.position },
projInv: { value: new THREE.Matrix4( ) },
viewInv: { value: new THREE.Matrix4( ) }
},
vertexShader:`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader:`
uniform float time;
uniform vec2 resolution;
uniform vec3 cameraPos;
uniform mat4 projInv;
uniform mat4 viewInv;
varying vec2 vUv;
// SDF primitives see https://iquilezles.org/articles/distfunctions/
// ...................................................................
float sdSphere( vec3 p, float s ) {
return length( p ) - s;
}
float sdTorus( vec3 p, vec2 t ) {
vec2 q = vec2( length( p.xz ) - t.x, p.y );
return length( q ) - t.y;
}
float sdRoundBox( vec3 p, vec3 b, float r ) {
vec3 q = abs( p ) - b + r;
return length( max( q, 0.0 ) ) + min( max( q.x, max( q.y, q.z ) ), 0.0 ) - r;
}
//....................................................................
// raymarching
float raymarch(vec3 cp, vec3 rd) { // cp: cameraPos, rd: ray direction
float t = 0.;
const int MAX_STEPS = 100; // try other values 10 ... 200
for (int i = 0; i < MAX_STEPS; i ++) {
vec3 pos = cp + rd * t; // new position on ray
float dSph = sdSphere( pos, 1.0 );
float dTor = sdTorus( pos, vec2( 0.9, 0.2 ) );
float dSub = max( -dSph, dTor ); // SDF Subtraction
float dRBox = sdRoundBox( pos, vec3( 0.3, 0.15, 2.1 ), 0.1 );
float d = min( dSub, dRBox );
if ( d < 0.001 ) return t; // hit
t += d;
if ( t > 100.0 ) break;
}
return -1.0; // no match
}
void main( ) {
vec2 ndc = vUv * 2.0 - 1.0; // conversion vUv (0..1) => NDC (-1..1)
vec4 clipPos = vec4( ndc, -1.0, 1.0 ); // clip space ray
vec4 viewPos = projInv * clipPos; // unproject into the viewspace
viewPos = viewPos / viewPos.w; // viewspace coordinates
vec4 worldPos = viewInv * viewPos; // unproject into the worldspace
// ray direction: from cameraPos to unprojected pixel
float distFactor = 0.25; // filling size
vec3 rd = normalize( worldPos.xyz - cameraPos * distFactor );
vec3 cp = cameraPos;
float t = raymarch( cp, rd );
vec3 col;
if ( t > 0.0 ) {
// hit: color depending on t
col = vec3( 0.5 + 0.5 * sin( t + time ),
0.5 + 0.5 * sin( t + time + 0.5 ),
0.5 + 0.5 * sin( t + time + 2.0 ) );
//col = vec3(0.5,0.7,0.4);
} else {
col = vec3( 0.7, 0.7, 0.7 ); // no hit: background color
}
gl_FragColor = vec4(col, 1.0);
}
`
};
const composer = new EffectComposer( renderer );
composer.addPass( new RenderPass( scene, camera ) );
const shaderPass = new ShaderPass( customShader );
shaderPass.renderToScreen = true;
composer.addPass( shaderPass );
const clock = new THREE.Clock( );
function animate( ) {
requestAnimationFrame( animate );
customShader.uniforms.time.value = clock.getElapsedTime( );
//customShader.uniforms.projInv.value.copy( camera.projectionMatrix ).invert( );
//customShader.uniforms.viewInv.value.copy( camera.matrixWorld );
composer.render( );
}
animate( );
</script>
</html>