感谢 https://blog.youkuaiyun.com/qq_41368247/article/details/106194092 的详尽分析,
这里使用的three.js 版本为106,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo5_1 距离场 圆</title>
<script src="../external/three.js"></script>
<script src="../controls/OrbitControls.js"></script>
<style>
body {
overflow: hidden;
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div id="container"></div>
<script id="vertexShader" type="x-shader/x-vertex">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
uniform vec2 u_resolution;
uniform float u_time;
varying vec2 vUv;
uniform vec2 u_mouse;
// Signed distance and circle
// List of some other 2D distances:
//
// Circle: https://www.shadertoy.com/view/3ltSW2
// Segment: https://www.shadertoy.com/view/3tdSDj
// Triangle: https://www.shadertoy.com/view/XsXSz4
// Isosceles Triangle: https://www.shadertoy.com/view/MldcD7
// Regular Triangle: https://www.shadertoy.com/view/Xl2yDW
// Regular Pentagon: https://www.shadertoy.com/view/llVyWW
// Regular Octogon: https://www.shadertoy.com/view/llGfDG
// Rounded Rectangle: https://www.shadertoy.com/view/4llXD7
// Rhombus: https://www.shadertoy.com/view/XdXcRB
// Trapezoid: https://www.shadertoy.com/view/MlycD3
// Polygon: https://www.shadertoy.com/view/wdBXRW
// Hexagram: https://www.shadertoy.com/view/tt23RR
// Regular Star: https://www.shadertoy.com/view/3tSGDy
// Star5: https://www.shadertoy.com/view/wlcGzB
// Ellipse 1: https://www.shadertoy.com/view/4sS3zz
// Ellipse 2: https://www.shadertoy.com/view/4lsXDN
// Quadratic Bezier: https://www.shadertoy.com/view/MlKcDD
// Uneven Capsule: https://www.shadertoy.com/view/4lcBWn
// Vesica: https://www.shadertoy.com/view/XtVfRW
// Cross: https://www.shadertoy.com/view/XtGfzw
// Pie: https://www.shadertoy.com/view/3l23RK
// Arc: https://www.shadertoy.com/view/wl23RK
// Horseshoe: https://www.shadertoy.com/view/WlSGW1
// Parabola: https://www.shadertoy.com/view/ws3GD7
// Parabola Segment: https://www.shadertoy.com/view/3lSczz
// Rounded X: https://www.shadertoy.com/view/3dKSDc
// Joint: https://www.shadertoy.com/view/WldGWM
// Simple Egg: https://www.shadertoy.com/view/Wdjfz3
//
// and many more here: http://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
float sdCircle( in vec2 p, in float r ) {
return length(p)-r;
}
float sdBox( in vec2 p, in vec2 b )
{
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
// return length(max(d,0.0)) + max(d.x,d.y);
// return max(d.x, max(d.y, 0.0)) + min(max(d.x,d.y),0.0);
}
float sdSegment( in vec2 p, in vec2 a, in vec2 b )
{
vec2 pa = p-a, ba = b-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length( pa - ba*h );
}
float ndot(vec2 a, vec2 b ) { return a.x*b.x - a.y*b.y; }
float sdRhombus( in vec2 p, in vec2 b) {
vec2 q = abs(p); // 坐标轴对称
// 计算的是线段b的中点到p点的向量,在b上的投影限制到[-1, 1]
// 负数表示向量偏向于y轴, 正数表示偏向x轴
float h = clamp((-2.0*ndot(q,b)+ndot(b,b))/dot(b,b),-1.0, 1.0);
/* 实际上h从-1到1的滑动过程,[0.5*b*vec2(1.0-h,1.0+h)] 表示一条从原点出发,
终点在向量b上由左上到右下的滑动的向量 */
float d = length( q - 0.5*b*vec2(1.0-h,1.0+h) );
// 符号:可计算(b.x, -b.y)和(p.x, p.y-b.y)的叉积,得两向量的相对位置
return d * sign( q.x*b.y + q.y*b.x - b.x*b.y );
}
float sdRhombus0( in vec2 p, in vec2 b) {
// 参考线段的SDF
vec2 a = vec2(0.0, b.y); // 与y轴交点
vec2 c = vec2(b.x, 0.0);
vec2 q = abs(p); // 坐标轴对称
vec2 pa = q-a, ba = c-a;
float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
// float h = clamp( dot(pa,ba)/dot(ba,ba), -1.0, 2.0 );
float d = length( pa - ba*h );
return d * sign( q.x*b.y + q.y*b.x - b.x*b.y );
}
/**
* 等边三角形: 1. 原点在三角形中心点
* 2. r表示边长的一半(不是中心点到顶点的距离)
*/
float sdEquilateralTriangle(in vec2 p, in float r ) {
const float k = sqrt(3.0);
p.x = abs(p.x);
// 由于三角形可以看成是由三个部分绕中心点三个角度生成的,x负半轴不用考虑
// 所以只需要考虑在区域 1的那部分,这一部分可以通过关于直线y=-1/sqrt(3)*x对称映射到下方的三分之一
// 映射关系如下:
if( p.x+k*p.y>0.0 ) p=vec2(p.x-k*p.y,-k*p.x-p.y)/2.0;
// 一切都归一到只需要看区域 2-3-4即可
// x在减去r后,若其值-2*r<x<0,说明原来范围是-r<0<r,即知道点p在x轴上处于三角形内部,只需看y轴长度即可
// 否则,需要原x轴坐标减去左/右下角坐标x值
p.x -= r;
p.y += r/k;
p.x -= clamp( p.x, -2.0*r, 0.0 );
// p.x = 0.0; p.y += r/k;
return -length(p)*sign(p.y);
}
/**
* 等腰三角形: 1. 原点位于顶角,垂直于底边的高和y轴重合
* 2. q表示右腰的向量,一般q.y为负(此时顶角是朝上的)
*/
float sdTriangleIsosceles( in vec2 p, in vec2 q )
{
p.x = abs(p.x);
// 分为两种可能的最小距离: 1.与腰的距离, 2.与底的距离
// 1. 与腰
vec2 a = p - q*clamp( dot(p,q)/dot(q,q), 0.0, 1.0 );
// 2. 与底
vec2 b = p - q*vec2( clamp( p.x/q.x, 0.0, 1.0 ), 1.0 );
float k = sign( q.y );
float d = min(dot( a, a ), dot( b, b ));
float s = max(k*(p.x*q.y-p.y*q.x), k*(p.y-q.y)); // 两项都小于零才是取负,即内部
// float s = k*(p.y-q.y)>0.0?((k*(p.x*q.y-p.y*q.x))>0.0?1.0:-1.0):-1.0; // 可用此句替换
return sqrt(d)*sign(s);
}
/**
* 一般三角形: 1. 原点不限
* 2. 三条边的向量都给出,为 p0,p1,p2
*/
float sdTriangle( in vec2 p, in vec2 p0, in vec2 p1, in vec2 p2 )
{
vec2 e0 = p1-p0, e1 = p2-p1, e2 = p0-p2;
vec2 v0 = p -p0, v1 = p -p1, v2 = p -p2;
// 分别计算过p点垂直于三条边的向量,箭头指向p点
vec2 pq0 = v0 - e0*clamp( dot(v0,e0)/dot(e0,e0), 0.0, 1.0 );
vec2 pq1 = v1 - e1*clamp( dot(v1,e1)/dot(e1,e1), 0.0, 1.0 );
vec2 pq2 = v2 - e2*clamp( dot(v2,e2)/dot(e2,e2), 0.0, 1.0 );
// 由于不清楚传入的三个点是顺时针还是逆时针顺序,先确定好基本符号
// 若s是-1,说明传入是逆时针
// 假设规定传入都是顺时针顺序,则s为1,可无视
float s = sign( e0.x*e2.y - e0.y*e2.x );
// 这里的 d并非具有什么实际几何意义的向量,而是作为一个数据的集合来使用
// 它的第一个分量d.x表示p点与各边长度的平方中的最小值,它的第二个分量可以用来判断内部外部
// 假如点P在三角形内部,那么参与比较的三个式子都会是正数,它们中的最小值也将是正数
// 若在外部,则至少有一个式子是负的,取最小值后d.y也将是负值
vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
// 若d.y为负数,说明在三角形外,不要漏看下面一行的负号
return -sqrt(d.x)*sign(d.y);
}
/**
* y轴对称的不均匀胶囊 1. 下半圆的圆心为原点
* 2. h表示两个半圆的圆心之间的距离, ra为下半圆半径
*/
float cro(in vec2 a, in vec2 b ) { return a.x*b.y - a.y*b.x; }
float sdUnevenCapsuleY( in vec2 p, in float ra, in float rb, in float h )
{
p.x = abs(p.x); // 左右对称
float cos_h = (ra-rb)/h;
float sin_h = sqrt(1.0-cos_h*cos_h);
// c是垂直于直线边的单位向量,直线边单位向量为(-cos_h, sin_h)
vec2 c = vec2(sin_h, cos_h);
// 所谓cro其实可以看成先将b逆时针旋转90°,然后再计算a和b的点乘
// 胶囊的sdf计算同样分为三部分,上半圆+直线边+下半圆, 方法是将op向量投影到直线边上
// 所以k就表示op投影到直线边上,可以用来判断p点处于哪块区域
float k = cro(c,p);
// m表示op投影到直线边逆时针旋转了90°的向量上,可用来计算p点与下圆圆心之间的距离
float m = dot(c,p);
// op长度的平方
float n = dot(p,p);
if( k < 0.0) return sqrt(n)- ra; // 位于下圆部分
// 上圆,这里用到了余弦定理,h表示两圆心的长度,以及op和p点到上半圆的连线组成一个三角形,sqrt(n)*cos(op与y轴夹角)=p.y (因为h在y轴上)
else if( k > c.x*h ) return sqrt(n+h*h-2.0*h*p.y)- rb;
// 减去ra
else return m - ra;
}
/**
* 普通的不均匀胶囊 1. 原点不固定
* 2. pa,pb表示两圆心坐标, ra,rb表示两圆半径
*/
float sdUnevenCapsule1( in vec2 p, in vec2 pa, in vec2 pb, in float ra, in float rb )
{
// 思路是坐标转化,将问题转化成y轴对称的胶囊
// 预计算
pb -= pa; // pa指向pb的向量
float h = sqrt(dot(pb,pb)); // 两圆心距离
// 1. 坐标平移:以pa作为原点的平移,相当于将胶囊的一个圆心平移到原点
p -= pa;
// 2. 坐标旋转:p.x = p.x* pb.y/h - p.y* pb.x/h
// p.y = p.x* pb.x/h + p.y* pb.y/h
// 实际上pb/h就是单位向量,其x分量就是cosθ,y分量就是sinθ
// 要将圆心连线旋转到与y轴平行,也就是逆时针旋转90-θ度,θ是连线与x轴夹角
/*
左乘旋转矩阵:
[cos(90-θ), -sin(90-θ)] [sinθ, -cosθ] [pb.y, -pb.x]
[sin(90-θ), cos(90-θ)] = [cosθ, sinθ] = [pb.x, pb.y]
*/
vec2 q = vec2( dot(p,vec2(pb.y,-pb.x)), dot(p,pb))/h;
// 调用Y轴对称版本 -----------
return sdUnevenCapsuleY(q, ra, rb, h);
}
void main( )
{
vec2 p = vUv * 2.0 - 1.0;
// float d = sdCircle(p,0.5);
float d = sdBox(p, vec2(0.4, 0.2));
// float d = sdSegment(p, vec2(0.3, 0.4), vec2(-0.4, -0.3));
// float d = sdRhombus0(p, vec2(0.6, 0.3));
// float d = sdEquilateralTriangle(p, 0.4);
// float d = sdTriangleIsosceles(p, vec2(0.1, -0.4));
// float d = sdTriangle(p, vec2(-0.3, -0.4), vec2(0.3, -0.4), vec2(0.2, 0.6));
// float d = sdUnevenCapsule1(p, vec2(-0.33, -0.21), vec2(0.25, 0.36), 0.4, 0.2);
// coloring
vec3 col = vec3(1.0) - sign(d)*vec3(0.1,0.4,0.7);
col *= 1.0 - exp(-3.0*abs(d));
col *= 0.8 + 0.2*cos(150.0*d); // + u_time*6.0
col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
gl_FragColor = vec4(col,1.0);
}
</script>
<script>
//https://blog.youkuaiyun.com/qq_41368247/article/details/106194092
var container;
var camera, scene, renderer;
var uniforms;
var mesh;
init();
animate();
function init() {
container = document.getElementById( 'container' );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.set(0, 0, 2);
scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(20));
var geometry = new THREE.PlaneBufferGeometry( 2, 2 );
uniforms = {
u_time: { type: "f", value: 1.0 },
u_resolution: { type: "v2", value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
u_mouse: { type: "v2", value: new THREE.Vector2(-1, -1) },
};
var material = new THREE.ShaderMaterial( {
uniforms: uniforms,
side: THREE.DoubleSide,
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent
} );
material.extensions.derivatives = true;
var material2 = new THREE.MeshBasicMaterial({color: '#00bbbb', wireframe: true, side: THREE.DoubleSide})
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
container.appendChild( renderer.domElement );
let orbit = new THREE.OrbitControls( camera, renderer.domElement );
onWindowResize();
window.addEventListener( 'resize', onWindowResize, false );
}
function onWindowResize( event ) {
renderer.setSize( window.innerWidth, window.innerHeight );
// uniforms.u_resolution.value.x = renderer.domElement.width;
// uniforms.u_resolution.value.y = renderer.domElement.height;
}
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
uniforms.u_time.value += 0.01; //0.008
renderer.render( scene, camera );
}
document.addEventListener('mousemove', onDocumentMouseMove, false);
function onDocumentMouseMove(event) {
let vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
vector = vector.unproject(camera);
let raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
let lst = raycaster.intersectObjects([mesh]);
if (lst.length == 0) {
return;
}
let curPos = lst[0].point;
// console.log(curPos);
let ux = (curPos.x+1)/2, uy = (curPos.y+1)/2;
uniforms.u_mouse.value.x = ux;
uniforms.u_mouse.value.y = uy;
// console.log("ux: " + ux + ", uy: " + uy);
}
</script>
</body>
</html>