更详细的介绍见:https://zhuanlan.zhihu.com/p/365440831
距离函数是什么?
距离函数顾名思义是一个和距离有关的函数,既然是函数,我们就可以用 f(x) 来表示。那么 f(x) 里面干了些什么呢?它会返回空间中任何一个点到物体表面的最短距离。
这么说可能有点不好理解,我们来看一个例子,如下图:
在二维空间中,黑色的圈就是我们圆的物体表面,圆心在原点,半径为r。而ABCD点,是我们空间中任意点的四个采样,我们来看看这些点到圆面上的最短距离d怎么算。
通过数学常识我们知道,任意点到圆面的最近距离,必然是该点与圆心的连线在圆上的交点与该点的距离,例如A点到圆上的最短距离就是AP的长度。那么PA的长度怎么求呢?因为我们圆心在原点,因此PA=OA-OP,而OP的长度就是半径r,因此PA=OA-r 。该式子同样适合于其他三个点。
也就是说我们可以得到一个函数 f(v)=|v|-r ,输入值v即为二维空间中任意点的坐标(一个二维向量),|v|就是向量v的模,代表到原点的长度/距离,r为圆的半径,那个这个函数就是半径为r,圆心在原点的圆的距离函数。
我们还可以发现,如果f(x)的返回值为0,那么这个点就在圆上,例如B点,也就是说我们只要通过这个函数,找出所有返回值为0的点,这些点就会形成一个圆。
此外对于D点而言,f(x)返回的是负数,这是因为点在圆内,OD<r,得到负数,也就是说圆内所有点到圆上的最短距离我们都可以认为是负的。
通过这个例子我们可以总结出,距离函数会返回空间中任何一个点到物体表面的最短距离,我们只需要找出距离为0的所有点,即可得到这个物体表面,而距离小于0的点代表在物体内部,距离大于0的点代表在物体外。对于这种带有正负的距离函数,我们也称为符号距离函数(Signed Distance Function,简称SDF)或定向距离函数(Oriented Distance Function,简称SDF)。距离函数在光线追踪中有着重要的作用,我们也可用距离函数来画一些几何体。
附:wiki的解释:https://en.wikipedia.org/wiki/Signed_distance_function
这里大家可能会问,前面的例子我们的圆的圆心正好在原点,所以可以得到 f(v)=|v|-r,那要是不是这样的怎么办?例如下图:
此时A到圆心的长度AH就不能用 |OA| 来代替了,那么我们怎么求AP的长度?这里我们可以应用平移变换的思想,不管圆心在哪,都是从原点平移了 (设该向量为h),空间中任意点同样也是平移了h,把这个平移值去掉就变成了了按原点计算,因此对于圆心在H,半径为R的圆,其距离函数为:f(v)=|v-h|-r 。
在Shader中使用距离函数绘制二维图形
既然要用Shader绘制,那么就要写Shader,这里推荐一个网站shadertoy(要翻墙),可以很容易让我们在网站上使用GLSL语言写一些Shader。同时里面也有位大神为我们提供了很多二维图形的距离函数,文档如下:
https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
我们先从最简单的例子来讲起。
距离函数画圆
在前面我们已经说到了圆的距离函数为,f(v)=|v-h|-r,那么在我们代码里,函数就是一段功能代码,我们用GLSL来表达的话,如下:
float drawCircle(in vec2 p, in vec2 offset, in float r)
{
return length(p-offset)-r;
}
先看输入的值,p代表任意点的坐标,offset即圆心和原点的偏移量,r就是圆的半径。
那么有个问题,offset和r我们很好设置,这个p怎么来?空间中任意点,那不就是无数个点了么?
采样空间中的任意点
对于上面的问题又用到了采样的概念,我们知道屏幕是由像素组成的,且我们的像素只有有一个颜色,而我们能看出物体表面也是因为这些像素显示了物体表面的颜色。那么任意点到物体表面的距离,不就可以简化为任意像素到物体表面的距离,如果距离为0,那么这个像素就应该显示物体表面的颜色。
也就是说我们只采样所有像素的中心点,带入距离函数即可。并且这个思路在Shader中很容易就可以实现,因为我们的Fragment Shader就是每个像素调用一次的。
在GLSL里,我们的Fragment Shader就是下面函数:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
}
fragCoord为像素坐标,最终像素的颜色保存在fragcolor里即可。可参考文档:https://www.shadertoy.com/howto
既然fragCoord代表的就是像素坐标,那么我们把它带入距离函数不就可以了,代码如下:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
float d = drawCircle(fragCoord, vec2(150.0,50.0), 80.0);
vec3 col = vec3(1.0);
if(d<-1.0) col = vec3(1.0,0.0,0.0);
if(d>1.0) col = vec3(0.0,1.0,0.0);
fragColor = vec4(col,1.0);
}
注:d不用0来判断是为了圆有个宽度,否则看不出。此外这些数字后面一定要加 .0 ,否则类型不匹配,编译不过。
得到的效果如下:
效果是对的,因为屏幕像素是从左下角为原点的。
利用uv坐标
上面的方法有个问题,那就是很难适配不同分辨率的屏幕,屏幕像素越多,这个圆在屏幕内的占比就会越小,如下图:
这个问题,我们可以用uv坐标来解决。
理解起来很简单,本来像素的横坐标是由0到width-1,纵坐标是0到height-1,例如我们像素坐标(80,80)就是该像素的真实坐标,它并不会由于width和height的变换而变换。但是如果我们使像素的横坐标和纵坐标都是0到1,也就是uv坐标,那么像素坐标(0.4,0.4)对应的真实坐标就会跟着屏幕大小而改变。
在GLSL中,提供了iResolution字段,可以让我们获取到屏幕的分辨率,那么我们将fragCoord除以它就是该像素对应的uv坐标(这里看着像是向量的除法运算,实则不是,因为向量没有除法运算,这种写法只是把两个向量的x和y互相相除),代码如下:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 p = fragCoord/iResolution.xy;
float d = drawCircle(p, vec2(0.3,0.2), 0.3);
vec3 col = vec3(1.0);
if(d<-0.003) col = vec3(1.0,0.0,0.0);
if(d>0.003) col = vec3(0.0,1.0,0.0);
fragColor = vec4(col,1.0);
}
注:需要注意使用uv后,对应的偏移,半径和圆宽度判断都要修改数值。
得到结果如下:
擦!圆怎么扁了?这是因为我们uv都是0到1,那么长宽不相等的情况下,uv取值相等的情况下得到的不就是一个扁的玩意嘛。
改!为了解决扁的问题,我们就要使uv的取值比值和分辨率的比值相等,例如分辨率200*100,v的取值是0到1的话,那么我们u的取值就应该是0到2。也就是说我们取较短的一边对应0到1,而较长的一边按比例往外扩,代码修改如下:
vec2 p = fragCoord/iResolution.xy;
if (iResolution.x > iResolution.y) {
p.x *= iResolution.x / iResolution.y;
} else {
p.y *= iResolution.y / iResolution.x;
}
float d = drawCircle(p, vec2(0.3,0.2), 0.3);
如果我们只考虑横屏情况的话,那么iResolution.x 肯定大于 iResolution.y,所以可以简化一下,为:
vec2 p = fragCoord/iResolution.xy;
p.x *= iResolution.x / iResolution.y;
float d = drawCircle(p, vec2(0.3,0.2), 0.3);
得到结果就正确了(如下图),并且即是分辨率不同了,比例也不会改变。
在屏幕中心绘制
上面我们都是以左下角为原点,但是往往更多的情况,我们希望以屏幕的中心作为原点来绘制。其实也很简单,我们只需要把uv的取值范围从0到1变为-1到1即可,这样取值为0时,代表的就是屏幕的中心点,代码如下:
vec2 p = fragCoord/iResolution.xy;
p = p*2.0-1.0;
p.x *= iResolution.x / iResolution.y;
float d = drawCircle(p, vec2(0.3,0.2), 0.3);
得到结果为:
偏移量即从屏幕中心开始计算,同时圆也变小了,因为我们圆的半径没变,但是取值范围增加了一倍,所以就变小了。
同时对于上面那一段代码,我们可以进行简写,简写如下:
vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
简单推一下:
我们先看 p.x:p.x = fragCoord.x/iResolution.x,又因为 p.x = p.x*2-1,所以 p.x = 2*fragCoord.x/iResolution.x-1 = (2*fragCoord.x-iResolution.x)/iResolution.x,然后又因为 p.x *= iResolution.x / iResolution.y,所以最终得到 p.x = (2*fragCoord.x-iResolution.x)/iResolution.y。
再来看看p.y:同理x,可得到 p.y = 2*fragCoord.y/iResolution.y-1 = (2*fragCoord.y-iResolution.y)/iResolution.y。
也就是说xy的式子是一样的,所以最终得到上面简写的式子。这也是shadertoy里面大神们常用的写法,不钻研下都特么的看不懂。
等高线
如图,图中多了很多颜色较深的圈(注意它们不是黑色,只是颜色比较深而已),任意一个圈上的任意一点,到我们物体表面(也就是例子中的圆)的最短距离都相等,这种线我们就称为等高线。
它是怎么画出来的呢?在最后新增下面代码即可:
col *= (0.8 + 0.2*cos(100.0*d));
因为我们的d取值很小,因此乘以100来使cos的值变化的更快,而cos的值永远都是0到1,也就是说 0.2*cos的值在0-0.2之间波动,那么0.8+0.2*cos 的值在0.8-1之间波动。即随着点到物体表面最短距离的距离变化,颜色的值在 col*0.8 和 col*1 之间变化,最短距离相同则颜色相同,图中暗色的线就是col*0.8的结果。又因为cos是周期函数,因此就出现了一条条的等高线。
Blend
待学习补充。。
其他的二维图形
其他的一些基本二维图形在前面给的大神文章里面基本都有介绍到,这里也就不过多的比比了。
使用Unity的Shader代替
由于Shadertoy需要翻墙,或者说网页里那些牛逼的效果我们想要用到Unity里,由于Unity是HLSL写的,因此我们需要把GLSL语言转换一下。在HLSL官方文档里介绍到了一些转换:https://docs.microsoft.com/zh-cn/windows/uwp/gaming/glsl-to-hlsl-reference
例如:
GLSL | HLSL |
iResolution | _ScreenParams |
vec2 | float2/fixed2 |
片段着色器:void mainImage( out vec4 fragColor, in vec2 fragCoord ) | 像素着色器:fixed4 frag (v2f i) : SV_Target |
mix() | lerp() |
屏幕坐标左下角为原点 | 屏幕坐标左上角为原点 |
我们这里简单的把前面绘制圆的GLSL代码转一下,如下:
Shader "Unlit/NewUnlitShader"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 pos:POSITION;
};
v2f vert (appdata v)
{
v2f o;
//将顶点坐标由对象空间转到屏幕空间
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
float drawCircle(in fixed2 p, in fixed2 offset, in float r)
{
return length(p-offset)-r;
}
fixed4 frag (v2f i) : SV_Target
{
//变为左下角为原点
i.pos.y = _ScreenParams.y - i.pos.y;
//插值得到的i.pos等价于fragCoord
fixed2 p = (2.0 * i.pos - _ScreenParams.xy) / _ScreenParams.y;
float d = drawCircle(p, fixed2(0.0,0.0), 0.5);
fixed3 col = fixed3(1.0,1.0,1.0);
if(d<-0.003) col = fixed3(1.0,0.0,0.0);
if(d>0.003) col = fixed3(0.0,1.0,0.0);
col *= (0.8 + 0.2*cos(100.0*d));
return fixed4(col,1.0);
}
ENDCG
}
}
}
然后我们用UGUI建个Image,拖上带有这个Shader的Material即可。
用距离函数绘制三维图形
在大神的这篇文章里:https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm,介绍了三维几何的距离函数,但是当我点第一个进去的时候,就蒙蔽了
这搞毛呀,其实想想也是,大家都是显示在二维平面上的一个圈,为什么你是圆我是球,无非就是着色给人带来的感觉,而着色又要和光照相关联。因此想要真正自己用距离函数绘制出三维的几何,还需要很多的学习。等我学会了再说~