最近年底没什么事情,要过年了嘛,放个大年假。我又捡起基础数学书看了,本来一直在看微分定积分偏微分计算,不过目前对于我来说应用场景不多,比如定积分和偏微分,在求面体积和数值分析方面大有可为,领域偏向于工业和研究领域,我还没想到怎么应用到我的日常开发中,如果我做教学软件就好了,数学施教类应该用的挺多(当然高级别的着色效果也用到微积分,主要还是我太水,知识深度和应用深度都还不够,估计还得要几年吧,反正我也不急,慢慢混,哈哈)。
刚刚拿着我的三角尺和自动铅笔听小说发呆,突然想起来怎么用shader写一个绘制三角形的功能,或者我们就通用一点,写个绘制多边形的功能。平时我们用数学描叙一个三角形或者多边形,就描叙其顶点,眨一看挺简单的。
不过图形学中,我们目前使用光删渲染管线,要实际显示在像素矩阵显示屏上,不仅仅是描叙数学符号而已。一个最实际的问题:如果一个网格的顶点和三角面通过MVP和透视除法和视图变化到屏幕像素矩阵,怎么绘制出来,我们假设屏幕二维坐标系中已经变换好了三(n)个顶点的坐标,怎么绘制出来三角(多边)形,这个也是删格化的处理方式。
我们前面已经知道显示屏绘制就是逐行逐列扫描,那么意味着我们可以判断像素点(0,0)到(m,n)是否在多边形内即可,当然我们肯定要优化一下算法的,判断从全屏幕改为多边形的外接矩形。
前面我们也写过了判断点在多边形中的计算方法,这里为了顺便扩展一下数学计算,我们用另外的判断方法:判断点在线段的两侧的哪一侧,我们假设P点在多边形内部,那么P点在多边形任意一条边(顺时针或逆时针)的同一侧,画个图:
假设我们按照顺时针,那么P点就在任意一条线段的右侧。
那么我们只需要判断点于直线的两侧关系了,再来一张图:
可以看出几种判断方法,如果P在OE线段所在直线的右侧:
1.角POX小于角EOX
2.OE和OP的差积朝向纸内(右手定则)
3.建立OE的直线方程L:Ax+By+C=0,将P.x和P.y带入L,得到结果D>0或D<0则表示P在哪两侧(以x轴为参考,一四象限内:D<0在右侧,二三象限内:D>0则在右侧,但是如果P于E不在同一象限,则判断情况根据y轴正负划分)
4.OP的斜率<OE的斜率(不过存在特殊情况,如果OE在一象限,OP在三象限OE的右侧,则OP的斜率>OE的斜率,余下不同象限多种判断情况)
这样我们就建立了计算方法了(我就用向量差积z分量去判断吧),下面开始写shader:
Shader "Custom/GridDrawImageEffectShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma exclude_renderers d3d11 gles
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
uniform vector points[8];
uniform int pointCount;
//isright是否正确
//只计算差积z分量就ok
bool isRight(float2 o, float2 e, float2 p)
{
float3 oe = float3(e.x - o.x, e.y - o.y, 0);
float3 op = float3(p.x - o.x, p.y - o.y, 0);
float z = oe.x * op.y - oe.y * op.x;
if (z < 0)
return true;
return false;
}
float minfloat(float farr[8],int len)
{
float min = 10000;
for(int i =0;i<len;i++)
{
if(min>farr[i])
{
min = farr[i];
}
}
return min;
}
float maxfloat(float farr[8],int len)
{
float max = 0;
for(int i =0;i<len;i++)
{
if(max < farr[i])
{
max = farr[i];
}
}
return max;
}
//uv pixel是否在外接矩形内
bool isRectangle(float2 uv)
{
float2 pp = float2(uv.x * _ScreenParams.x,uv.y*_ScreenParams.y);
float xarr[8];
float yarr[8];
for(int i = 0;i<pointCount;i++)
{
float2 p = points[i];
xarr[i] = p.x;
yarr[i] = p.y;
}
float left = minfloat(xarr,pointCount);
float right = maxfloat(xarr,pointCount);
float bottom = minfloat(yarr,pointCount);
float top = maxfloat(yarr,pointCount);
if(pp.x<left || pp.x>right || pp.y<bottom || pp.y>top)
{
return false;
}
return true;
}
//顺时针判断uv pixel是否在多边形内
bool isInside(float2 uv)
{
float2 pp = float2(uv.x * _ScreenParams.x,uv.y*_ScreenParams.y);
for(int i = 0;i<pointCount;i++)
{
float2 fp = float2(points[i].x,points[i].y);
float2 tp;
if(i<(pointCount-1))
{
tp = float2(points[i+1].x,points[i+1].y);
}else
{
tp = float2(points[0].x,points[0].y);
}
if(!isRight(fp,tp,pp))
{
return false;
}
}
return true;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col;
if(isRectangle(i.uv) )
{
if(isInside(i.uv))
col = fixed4(0,1,0,1);
else
col = fixed4(0,0,1,1);
}
else
{
col = fixed4(0,0,0,1);
}
return col;
}
ENDCG
}
}
}
c#代码(相当于提交dc):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GridEffect : MonoBehaviour
{
[SerializeField] private Material material;
void Start()
{
List<Vector4> points = new List<Vector4>
{
new Vector4(400,80,0,0),
new Vector4(200,300,0,0),
new Vector4(500,800,0,0),
new Vector4(700,400,0,0),
new Vector4(600,70,0,0),
};
material.SetInt("pointCount", points.Count);
material.SetVectorArray("points", points.ToArray());
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination, material);
}
}
效果如下:
解释一下:
1.计算点与线段两侧关系
2.计算外接矩形,绘制外接矩形蓝色
3.逐行逐列扫描uv(也就是像素)绘制多边形绿色
这个也是删格化的常用算法之一(我代码肯定写的不如大佬们高效,哈哈)。
下面继续,既然多边形绘制完毕,再尝试一下线段,其实线段的绘制和多边形一样就是删格化绘制,不同的是, 线段只有两个顶点,我们需要将顶点扩展成矩形,因为是删格化所以我们线段有宽度(不仅仅只是数学符号),就可以计算出线段组成的矩形的四个顶点,如图:
从P0P1两点扩展出adcb顺时针四个矩形顶点,求出四个点的坐标然后再绘制多边形区域就行了,计算垂线以前也算过,不清楚的话可以返回之前查看,这里直接上代码:
void Start()
{
Vector4 from = new Vector4(400, 80, 0, 0);
Vector4 to = new Vector4(700, 400, 0, 0);
Vector4 sta = new Vector4(from.x, from.y);
Vector4 end = new Vector4(to.x, to.y);
float k = (end.y - sta.y) / (end.x - sta.x);
float m = halfWidth / Mathf.Sqrt(1 + k * k);
float n = Mathf.Abs(k) * halfWidth / Mathf.Sqrt(1 + k * k);
#if UNITY_EDITOR
Debug.LogFormat("n={0} m={1}", n, m);
#endif
Vector4 a = sta + new Vector4(-n, m);
Vector4 b = sta + new Vector4(n, -m);
Vector4 c = end + new Vector4(n, -m);
Vector4 d = end + new Vector4(-n, m);
#if UNITY_EDITOR
Debug.LogFormat("a={0} b={1} c={2} d={3}", a, b, c, d);
#endif
//顺时针
List<Vector4> points = new List<Vector4>
{
a,d,c,b
};
material.SetInt("pointCount", points.Count);
material.SetVectorArray("points", points.ToArray());
}
这里我直接用c# start计算直线顶点了,如果需要动态变化就使用update或者shader中计算,不然就不需要消耗那么多性能,效果如图:
这些二维计算有助于我们理解删格化,同时应用范围也算是挺广的,ok,继续撸数学。
羊羊2035 发布了100 篇原创文章 · 获赞 101 · 访问量 10万+ 私信 关注