Shader中使用距离函数(Distance Function)绘制二维图形

更详细的介绍见: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的长度?这里我们可以应用平移变换的思想,不管圆心在哪,都是从原点平移了\vec{OH} (设该向量为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

例如:

GLSLHLSL
iResolution_ScreenParams
vec2float2/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,介绍了三维几何的距离函数,但是当我点第一个进去的时候,就蒙蔽了

这搞毛呀,其实想想也是,大家都是显示在二维平面上的一个圈,为什么你是圆我是球,无非就是着色给人带来的感觉,而着色又要和光照相关联。因此想要真正自己用距离函数绘制出三维的几何,还需要很多的学习。等我学会了再说~

 

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
使用Shader绘制网格,需要在Unity创建一个材质(Material)并将其分配给网格对象(Mesh)。材质包含着Shader的代码,它会告诉Unity如何绘制网格。 以下是一个简单的示例,演示如何使用Shader绘制一个简单的网格: 1. 创建一个新的Shader 在Unity,选择"Create" -> "Shader"创建一个新的Shader。选择一个合适的命名,例如"MyShader"。 2. 编写Shader代码 打开新创建的Shader,你会看到一个空白的代码文件。在这里,你可以自由编写你的Shader代码。以下是一个简单的示例,它会绘制一个灰色的网格: ``` Shader "Custom/MyShader" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags {"Queue"="Transparent" "RenderType"="Opaque"} Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv) * 0.5; } ENDCG } } } ``` 这个Shader包含一个名为"_MainTex"的属性,它是一个2D纹理。在Pass块Shader使用这个纹理对网格进行着色,并将颜色乘以0.5,以便将其变为灰色。 3. 创建一个材质 现在,你需要创建一个材质(Material),并将Shader分配给它。在Unity,选择"Create" -> "Material"创建一个新的材质。给它一个合适的名称,例如"MyMaterial"。 选择新创建的材质,将Shader分配给它。在Inspector窗口,将Shader字段设置为新创建的Shader。 4. 将材质分配给网格对象 最后一步是将材质分配给网格对象。在Unity,选择你想要绘制的网格对象,并将新创建的材质分配给它。 现在,当你在场景查看网格对象时,它应该使用你编写的Shader进行着色了。如果需要,你可以进一步调整你的Shader代码,以实现更复杂的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值