写在前面
看了几篇关于用shadertoy画线与画点的文章之后,突然想自己做一个五角星的效果来练练手。但是想归想,动起手来还是充满了“坎坷”。折腾了一个周末只是思路清晰,但代码却一塌糊涂,最后还是老老实实上网查阅各种公式资料,中途竟发现自己连求圆上点的公式都忘了,不禁感慨自己的数学真的已经还给老师了。唯一觉得欣慰的是最后东西是弄出来了,在shadertoy上实现的效果是这样的,地址在这里。
在shader中画点是用画圆的逻辑来做的,具体实现可以参照上一篇;而线段的实现代码是参考shadertoy网站的大神的,地址在这里的line()方法,直接理解起来比较繁琐,这画线的原理我搜遍百度都没查到究竟,最后破罐破摔竟然在一个游戏群里问到了答案,真是高手隐于市啊。。。具体原理说明会在后面的实现代码里注释。
shader里每画一个圆一条线都会占用到一个图层,最后需要将这些图层按我们想要的逻辑叠加起来,所以最后显示的这么一个效果的计算量是十分大的,现阶段也没想到好的办法来优化这些计算上性能的损耗,只能希望在之后的学习中摸索出来了。。。
原理说明
下面说下实现这个五角星的原理。
有一定的数学基本都会知道(大概吧)。正五角星的五个点都同一个圆上,每个相邻的点都相差72°,那么个以一个点为圆心,用求圆上点的公式算出五个点的坐标位置。而五角星有五条边,只要分别让当前的点与相隔的一个点画上线,那么就可以组成一个五角星。上个图直观一点吧
原理知道了接下来是在shader实现的方法.首先是画点的方法,其实也就是画圆的方法,代码如下所示
//画点
vec4 circle(vec2 pos, vec2 center, float radius, vec4 color) {
//求点是否在圆的半径内
float d = length(pos - center) - radius;
//fwidth(x) ==abs(ddx(x)) + abs(ddy(x)),对点求偏导,这种处理能让数据变平滑
float w = fwidth(0.5*d) * 2.0;
//图层0 画圆外边框
vec4 layer0 = vec4(_OutlineColor.rgb, 1.0-smoothstep(-w, w, d - _Antialias));
//图层1 画内圆
vec4 layer1 = vec4(color.rgb, 1.0-smoothstep(0.0, w, d));
//混合两个图层并返回
return mix(layer0, layer1, layer1.a);
}
原理我在上一篇已经讲过,这里用两个图层来画圆,其中一个图层处理圆的外边框颜色,另一个用来画圆的颜色。
接下来就是画线段的方法。之前看了candycat的画线方法,是利用斜率来画的,先贴上代码原理
vec4 DrawLines(vec2 pos,vec2 point1,vec2 point2,float width,float3 color,float antialias){
//斜率
float k=(point1.y-point2.y)/(point1.x-point2.x);
//y=kx+b 常量b=y-kx
float b=point1.y-k*point1.x;
//求点到直线的距离
// b=(kx-y+b)/sqrt(k*k+1*1)
float d=abs(k*pos.x-pos.y+b)/sqrt(k*k+1);
//Width/2 是因为要求两端的距离 antialias为平滑处理的范围
float t=smoothstep(width/2.0,width/2.0+antialias,d);
return vec4(color,1.0-t);
}
这个是通过求两点的斜率,然后用距离公式来判断点到线的距离来画线,理解起来十分容易,但有个问题是只能画直线!!不是线段!!如果用它来画线的话实现的效果是这样的
只要画面上的点跟斜率符合都会绘制上去,所以不适合我们的效果。最后在shadertoy的网站找到了个十分厉害的画线段方法这个画线是利用向量来计算点到线的距离的,实现的原理可以参考这篇文章的介绍,最后也附上个人的一点理解
其中推算的结果与下面代码运算的部分是相同的
vec2 dir0 = point2 - point1;
vec2 dir1 = pos - point1;
//dot()方法返回两个向量的点积 如果向量垂直返回0,平行返回1 相反返回-1
//clamp()方法限制返回0到1 截出线段,不然会返回直线
//这公式返回点到线上的距离
float h = clamp(dot(dir1, dir0)/dot(dir0, dir0), 0.0, 1.0);
//判断点是否在线的两边范围内
float d = (length(dir1 - dir0 * h) - width * 0.5);
代码中的dir0对应b,代码中的dir1对应a,最后要求的距离h对应向量d的长度。
最终整合出来的画线段方法是这样的
//画线
vec4 line(vec2 pos, vec2 point1, vec2 point2, float width) {
//分别求出点二到点一以及当前点到点一的向量
vec2 dir0 = point2 - point1;
vec2 dir1 = pos - point1;
//dot()方法返回两个向量的点积 如果向量垂直返回0,平行返回1 相反返回-1
//clamp()方法限制返回0到1 截出线段,不然会返回直线
//这公式返回点到线上的距离
float h = clamp(dot(dir1, dir0)/dot(dir0, dir0), 0.0, 1.0);
//判断点是否在线的两边范围内
float d = (length(dir1 - dir0 * h) - width * 0.5);
//平滑处理
float w = fwidth(0.5*d) * 2.0;
//画线的外边
vec4 layer0 = vec4(_OutlineColor.rgb, 1.-smoothstep(-w, w, d - _Antialias));
//画线
vec4 layer1 = vec4(_FrontColor.rgb, 1.-smoothstep(-w, w, d));
//混合两个图层
return mix(layer0, layer1, layer1.a);
}
最难的部分已经解决了,剩下的部分就轻松了。再来说说要画哪五个点。可以以坐标原点为圆心,五角星的五个点都是经过圆心的,那么处理的逻辑是这样的:假设第一个点的度数为a,则下一个点为a+72°,再下来个点是a+72°+72°,以此类推,最后将这些点保存到一个数组里面。当然也可以不保存数组直接画图层,但这样有个问题是线在后面的图层绘制,就会显示在点的上面不太好看。
在这里要重点提一下,cos()与sin()方法,对应的输入参数是弧度!!是弧度!!不是度数!!!
degree[i+1]=vec2(cos(d),sin(d);
上面这样写是错误的写法,得到的效果会十分奇怪,
正确的写法应该这样
degree[i+1]=vec2(cos(d*pi/180.0),sin((d*pi)/180.0));
这样的话将公式稍稍修改就可以得到新的效果,例如将sin部分缩放0.5可以得到缩放的五角星
将参数d与运行时间关联上的话就可以让五角星旋转起来~
最后附上Shadertoy与Unity的完整代码。
ShaderToy部分
vec4 _OutlineColor = vec4(1.0,1.0,1.0,1.0);
vec4 _FrontColor = vec4(1,0,0,1.0);
float pi=3.14159;
float _Antialias=0.01;
//画点
vec4 circle(vec2 pos, vec2 center, float radius, vec4 color) {
//求点是否在圆的半径内
float d = length(pos - center) - radius;
//fwidth(x) ==abs(ddx(x)) + abs(ddy(x)),对点求偏导,这种处理能让数据变平滑
float w = fwidth(0.5*d) * 2.0;
//图层0 画圆外边框
vec4 layer0 = vec4(_OutlineColor.rgb, 1.0-smoothstep(-w, w, d - _Antialias));
//图层1 画内圆
vec4 layer1 = vec4(color.rgb, 1.0-smoothstep(0.0, w, d));
//混合两个图层并返回
return mix(layer0, layer1, layer1.a);
}
//画线
vec4 line(vec2 pos, vec2 point1, vec2 point2, float width) {
//分别求出点二到点一以及当前点到点一的向量
vec2 dir0 = point2 - point1;
vec2 dir1 = pos - point1;
//dot()方法返回两个向量的点积 如果向量垂直返回0,平行返回1 相反返回-1
//clamp()方法限制返回0到1 截出线段,不然会返回直线
//这公式返回点到线上的距离
float h = clamp(dot(dir1, dir0)/dot(dir0, dir0), 0.0, 1.0);
//判断点是否在线的两边范围内
float d = (length(dir1 - dir0 * h) - width * 0.5);
//平滑处理
float w = fwidth(0.5*d) * 2.0;
//画线的外边
vec4 layer0 = vec4(_OutlineColor.rgb, 1.-smoothstep(-w, w, d - _Antialias));
//画线
vec4 layer1 = vec4(_FrontColor.rgb, 1.-smoothstep(-w, w, d));
//混合两个图层
return mix(layer0, layer1, layer1.a);
}
//根据index来保存图层的颜色值
void setlayer(inout vec4 layer[5],int index,vec4 val){
if(index==0)
layer[0]=val;
if(index==1)
layer[1]=val;
if(index==2)
layer[2]=val;
if(index==3)
layer[3]=val;
if(index==4)
layer[4]=val;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y;
//动态背景颜色
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
fragColor=vec4(col,1.0);
//点的图层
vec4 layers[5];
float d=iTime*10.0;
//保存五个点 从1开始
vec2 degree[6];
//for循环创建五个点
for(int i=0;i<=4;i++)
{
//保存点
//坐标上圆边上的点的坐标(cos(r),sin(r)) r为弧度
degree[i+1]=vec2(cos(d*pi/180.0),sin((d*pi)/180.0));
//绘制点
setlayer(layers,i,circle(uv,degree[i+1],0.06,_FrontColor));
//圆上的五角星,每个点相隔72度
d+=72.0;
}
//for循环画五条线
for(int i=1;i<6;i++){
vec2 point1=vec2(0.0,0.0);
//判断连线的位置 即当前点的隔一个点
if(i<=2)
{
point1=degree[i+3];
}
else
{
point1=degree[i-2];
}
//画线
vec4 temp=line(uv,degree[i],point1,0.02);
//混合线的图层
fragColor=mix(fragColor,temp,temp.a);
}
//混合点的图层
for (int i = 4; i >= 0; i--) {
fragColor = mix(fragColor, layers[i], layers[i].a);
}
}
Unity部分
Shader "Custom/pentagram" {
Properties {
//xy表示圆心在屏幕中的uv值,z为半径,w为圆边缘的平滑值
_OutlineColor("circleParameter",COLOR)=(0.5,0.5,10,0)
_FrontColor("circleColor",COLOR)=(1,1,1,1)
_Antialias("_Antialias",Range(0,1))=0.01
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#define vec2 float2
#define vec3 float3
#define vec4 float4
#define mat2 float2
#define mat3 float3
#define mat4 float4
#define iGlobalTime _Time.y
#define mod fmod
#define mix lerp
#define fract frac
#define Texture2D tex2D
#define iResolution _ScreenParams
#define pi 3.1415926
float4 _OutlineColor;
float4 _FrontColor;
float _Antialias;
struct v2f{
float4 pos:SV_POSITION;
float4 srcPos:TEXCOORD0;
};
//画点
vec4 circle(vec2 pos, vec2 center, float radius, vec4 color) {
float d = length(pos - center) - radius;
float w = fwidth(0.5*d) * 2.0;
vec4 layer0 = vec4(_OutlineColor.rgb, 1.-smoothstep(-w, w, d - _Antialias));
vec4 layer1 = vec4(color.rgb, 1.-smoothstep(0., w, d));
return mix(layer0, layer1, layer1.a);
}
//画线
vec4 lines(vec2 pos, vec2 point1, vec2 point2, float width) {
vec2 dir0 = point2 - point1;
vec2 dir1 = pos - point1;
float h = clamp(dot(dir0, dir1)/dot(dir0, dir0), 0.0, 1.0);
float d = (length(dir1 - dir0 * h) - width * 0.5);
float w = fwidth(0.5*d) * 2.0;
vec4 layer0 = vec4(_OutlineColor.rgb, 1.-smoothstep(-w, w, d - _Antialias));
vec4 layer1 = vec4(_FrontColor.rgb, 1.-smoothstep(-w, w, d));
return mix(layer0, layer1, layer1.a);
}
void setlayer(inout vec4 layer[5],int index,vec4 val){
if(index==0)
layer[0]=val;
if(index==1)
layer[1]=val;
if(index==2)
layer[2]=val;
if(index==3)
layer[3]=val;
if(index==4)
layer[4]=val;
}
v2f vert(appdata_base v){
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.srcPos=ComputeScreenPos(o.pos);
o.srcPos=o.pos;
return o;
}
vec4 main(vec2 fragCoord);
float4 frag(v2f iParam):COLOR{
//获取uv对应的当前分辨率下的点 uv范围(0-1) 与分辨率相乘
vec2 fragCoord=((iParam.srcPos.xy/iParam.srcPos.w)*_ScreenParams.xy);
return main(fragCoord);
}
vec4 main(vec2 fragCoord){
//vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y;
vec2 uv=fragCoord/iResolution.y;
vec3 col = 0.5 + 0.5*cos(iGlobalTime+uv.xyx+vec3(0,2,4));
vec4 layers[5];
float d=iGlobalTime*20.0;
vec2 degree[6];
for(int i=0;i<=4;i++)
{
degree[i+1]=vec2(cos(d*pi/180.0),sin((d*pi)/180.0));
setlayer(layers,i,circle(uv,degree[i+1],0.06,_FrontColor));
d+=72.0;
}
vec4 fragColor=vec4(col,1.0);
for(int i=1;i<6;i++){
vec2 point1=vec2(0.0,0.0);
if(i<=2)
{
point1=degree[i+3];
}
else
{
point1=degree[i-2];
}
vec4 temp=lines(uv,degree[i],point1,0.02);
fragColor=mix(fragColor,temp,temp.a);
}
for (int i = 4; i >= 0; i--) {
fragColor = mix(fragColor, layers[i], layers[i].a);
}
return fragColor;
}
ENDCG
}
}
FallBack "Diffuse"
}
总结
数学的部分真的很累人。。。大学的时候真不应该在数学上偷懒啊。
在浏览过一些大牛制作的shadertoy后,再一次感受到了shader的魅力,同时也深刻了解到自己知识面的不足。不多说了,加油吧!