正好用上圆相交计算,来记录一下。
问题:求Pa为圆心Ra为半径与Pb为圆心Rb为半径的两圆相交顶点P1、P2,顺便绘制相交区域。如图:
首先我们要求出P1、P2交点的坐标。我们假设圆A(圆心Pa、半径Ra),圆B(圆心Pb、半径Rb)方程:
这种方程其实解起来参数较多,挺麻烦的,我们可以使用相对坐标求解,将Pa(及Pb)平移到原点O(计算完毕再平移回来),那么Pa’=(0,0),Pb‘=Pb-Pa,则方程简化如下:
根据y的一元二次方程及求根公式可以得到y1、y2,然后带入x=(p-wy)/v中得到x1、x2,用c#实现一个函数就能计算出来了,如下:
private Vector2[] GetPositiveCrossPoint(Vector2 pa, float ra, Vector2 pb, float rb)
{
Vector2 pb1 = pb - pa;
Vector2[] cps = GetRelativeCrossPoint(ra, pb1, rb);
if (cps != null)
{
for (int i = 0; i < cps.Length; i++)
{
cps[i] += pa;
}
}
return cps;
}
private Vector2[] GetRelativeCrossPoint(float ra, Vector2 pb, float rb)
{
float a = pb.x;
float b = pb.y;
//2ax+2by=a2+b2+ra2+rb2
float v = 2 * a;
float w = 2 * b;
float p = a * a + b * b + ra * ra - rb * rb;
//x=(p-wy)/v
//x2+y2=ra2
//(w2+v2)y2-2pwy+(p2-ra2v2)=0
//ax2+bx+c=0
float arga = w * w + v * v;
float argb = -2 * p * w;
float argc = p * p - ra * ra * v * v;
float dta = argb * argb - 4 * arga * argc;
if (dta < 0)
{
#if UNITY_EDITOR
Debug.LogErrorFormat("方程无解");
#endif
return null;
}
float y1 = (-argb + Mathf.Sqrt(dta)) / (2 * arga);
float y2 = (-argb - Mathf.Sqrt(dta)) / (2 * arga);
float x1 = (p - w * y1) / v;
float x2 = (p - w * y2) / v;
Vector2[] cps = new Vector2[]
{
new Vector2(x1,y1),
new Vector2(x2,y2),
};
return cps;
}
中间用代数方式(也就是字母代替复杂的计算),刚好配合代码就能很方便的描绘出解法。接下来写一个demo验证一下:
public Transform pa;
[Range(0, 10)]
public float ra;
public Transform pb;
[Range(0, 10)]
public float rb;
void Start()
{
}
void Update()
{
Vector2[] cps = GetPositiveCrossPoint(pa.position, ra, pb.position, rb);
if (cps != null)
{
Vector2 p1 = cps[0];
Vector2 p2 = cps[1];
Debug.DrawLine(pa.position, p1, Color.red);
Debug.DrawLine(pb.position, p1, Color.red);
Debug.DrawLine(pa.position, p2, Color.green);
Debug.DrawLine(pb.position, p2, Color.green);
}
DrawCircle(pa.position, ra, Color.white);
DrawCircle(pb.position, rb, Color.black);
}
/// <summary>
/// 微分离散的绘制circle的边
/// </summary>
/// <param name="p"></param>
/// <param name="r"></param>
/// <param name="col"></param>
private void DrawCircle(Vector2 p, float r, Color col)
{
int ang = 0;
int step = 5;
int count = 360 / step;
Vector3[] poses = new Vector3[count];
for (int i = 0; i < count; i++)
{
float x = Mathf.Cos(ang * Mathf.Deg2Rad) * r + p.x;
float y = Mathf.Sin(ang * Mathf.Deg2Rad) * r + p.y;
poses[i] = new Vector3(x, y, 0);
ang += step;
}
for (int i = 0; i < count - 1; i++)
{
Vector3 pf = poses[i];
Vector3 pt = poses[i + 1];
#if UNITY_EDITOR
Debug.DrawLine(pf, pt, col);
#endif
}
}
效果如下:
接下来我们需要将黑白圆形相交的区域绘制出来(使用texture2d+pixels):绘制圆Pa和圆Pb的相交区域。因为栅格化就是逐行逐列扫描的过程,所以我们使用Pa和Pb组成的外接矩形进行“栅格化填充”。
首先我们绘制圆形:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CircleCrossPaint : MonoBehaviour
{
public RawImage image;
private int texWidth;
private int texHeight;
private Texture2D tex2d;
private Color clearColor;
private Color rectColor;
private Color circleColor;
private Color sectorColor;
private Color crossColor;
public Vector2 pa;
public int ra;
public Vector2 pb;
public int rb;
void Start()
{
InitTex();
DrawOutRect(pa, ra);
DrawCircle(pa, ra);
ApplyTex();
}
private void InitTex()
{
texWidth = Screen.width;
texHeight = Screen.height;
tex2d = new Texture2D(texWidth, texHeight, TextureFormat.ARGB32, false);
clearColor = new Color(0, 0, 0, 0);
rectColor = Color.black;
circleColor = Color.white;
sectorColor = Color.yellow;
crossColor = Color.green;
int colsLen = texWidth * texHeight;
Color[] cols = new Color[colsLen];
for (int i = 0; i < colsLen; i++)
{
cols[i] = clearColor;
}
tex2d.SetPixels(cols);
tex2d.Apply();
image.texture = tex2d;
}
private void DrawOutRect(Vector2 p, int r)
{
int top = (int)p.y + r;
int bottom = (int)p.y - r;
int left = (int)p.x - r;
int right = (int)p.x + r;
top = Mathf.Clamp(top, 0, texHeight - 1);
bottom = Mathf.Clamp(bottom, 0, texHeight - 1);
left = Mathf.Clamp(left, 0, texWidth - 1);
right = Mathf.Clamp(right, 0, texWidth - 1);
int wid = right - left;
int hei = top - bottom;
int rectlen = wid * hei;
Color[] rectcols = new Color[rectlen];
Vector2 c = new Vector2(wid / 2, hei / 2);
for (int i = 0; i < rectlen; i++)
{
rectcols[i] = rectColor;
}
tex2d.SetPixels(left, bottom, wid, hei, rectcols);
}
private void DrawCircle(Vector2 p, int r)
{
int top = (int)p.y + r;
int bottom = (int)p.y - r;
int left = (int)p.x - r;
int right = (int)p.x + r;
top = Mathf.Clamp(top, 0, texHeight - 1);
bottom = Mathf.Clamp(bottom, 0, texHeight - 1);
left = Mathf.Clamp(left, 0, texWidth - 1);
right = Mathf.Clamp(right, 0, texWidth - 1);
int wid = right - left;
int hei = top - bottom;
int rectlen = wid * hei;
Color[] rectcols = tex2d.GetPixels(left, bottom, wid, hei);
Vector2 c = new Vector2(wid / 2, hei / 2);
for (int x = 0; x < wid; x++)
{
for (int y = 0; y < hei; y++)
{
Vector2 xy2c = new Vector2(x, y) - c;
if ((xy2c.x * xy2c.x + xy2c.y * xy2c.y) < r * r)
{
rectcols[y * wid + x] = circleColor;
}
}
}
tex2d.SetPixels(left, bottom, wid, hei, rectcols);
}
private void ApplyTex()
{
tex2d.Apply();
}
}
绘制如下:
接下来绘制圆相交:
DrawCircleColor(px, rx, Color.white);
DrawCircleColor(py, ry, Color.black);
DrawCircleCross(px, rx, py, ry);
private void DrawCircleCross(Vector2 pa, float ra, Vector2 pb, float rb)
{
//计算两圆的外接矩形
int[] paout = GetOutRect(pa, (int)ra);
int[] pbout = GetOutRect(pb, (int)rb);
//计算两圆公共外接矩形
int left = paout[0] < pbout[0] ? paout[0] : pbout[0];
int right = paout[1] > pbout[1] ? paout[1] : pbout[1];
int bottom = paout[2] < pbout[2] ? paout[2] : pbout[2];
int top = paout[3] > pbout[3] ? paout[3] : pbout[3];
int wid = right - left;
int hei = top - bottom;
Color[] rectcols = tex2d.GetPixels(left, bottom, wid, hei);
for (int x = left; x < right; x++)
{
for (int y = bottom; y < top; y++)
{
Vector2 xy = new Vector2(x, y);
if (isInCircle(xy, pa, ra) && isInCircle(xy, pb, rb))
{
rectcols[(y - bottom) * wid + (x - left)] = crossColor;
}
}
}
tex2d.SetPixels(left, bottom, wid, hei, rectcols);
}
private bool isInCircle(Vector2 xy, Vector2 p, float r)
{
Vector2 xy2p = xy - p;
if ((xy2p.x * xy2p.x + xy2p.y * xy2p.y) <= (r * r))
{
return true;
}
return false;
}
效果如下:
顺便绘制一下扇形,扇形我们使用Axis+Angle绘制,如图:
我们根据axis和θ求出p1、p2,然后根据外接矩形(外接矩形Ptop为圆y轴最高顶点,Pbottom为p、p1、p2最低点,当然如果p1、p2处于第三四象限,那么外接矩形端点不同,所以我们是用整个圆外接矩形)“栅格化”判断Pxy是否处于扇形p1pp2中(也就是PxyPP1的夹角小于θ)。
DrawSector(pb, rb, new Vector2(1, 0), sectorAngle);
/// <summary>
/// 以axis为起始向量
/// ang为逆时针旋转的角度
/// 绘制扇形
/// </summary>
/// <param name="pc"></param>
/// <param name="r"></param>
/// <param name="aixs"></param>
/// <param name="ang"></param>
private void DrawSector(Vector2 p, int r, Vector2 axis, float ang)
{
int top = (int)p.y + r;
int bottom = (int)p.y - r;
int left = (int)p.x - r;
int right = (int)p.x + r;
top = Mathf.Clamp(top, 0, texHeight - 1);
bottom = Mathf.Clamp(bottom, 0, texHeight - 1);
left = Mathf.Clamp(left, 0, texWidth - 1);
right = Mathf.Clamp(right, 0, texWidth - 1);
int wid = right - left;
int hei = top - bottom;
int rectlen = wid * hei;
Color[] rectcols = tex2d.GetPixels(left, bottom, wid, hei);
Vector2 c = new Vector2(wid / 2, hei / 2);
for (int x = 0; x < wid; x++)
{
for (int y = 0; y < hei; y++)
{
Vector2 xy2c = new Vector2(x, y) - c;
if ((xy2c.x * xy2c.x + xy2c.y * xy2c.y) < (r * r))
{
float pang = AngleNormalize(Vector2.SignedAngle(axis, xy2c));
if (pang < ang)
{
rectcols[y * wid + x] = sectorColor;
}
}
}
}
tex2d.SetPixels(left, bottom, wid, hei, rectcols);
}
private float AngleNormalize(float ang)
{
if (ang >= 0)
{
ang %= 360;
}
else
{
ang = ((ang %= 360) + 360);
}
return ang;
}
效果如下:
顺便绘制圆Pa上扇形P1PaP2,如下:
圆Pa的Axis为PaP2向量,Ang为2*θ,PaP2(PaP1)与PaPb的夹角正负就可以判断哪个是Axis。同理圆Pb一样,根据PbP1(PbP2)与PbPa的夹角正负来计算。
private void DrawSectorWithCrossPoint(Vector2 pa, float ra, Vector2 pb, float rb)
{
Vector2[] cps = GetPositiveCrossPoint(pa, ra, pb, rb);
if (cps == null)
{
return;
}
Vector2 PaPb = pb - pa;
float PbPaCp0 = Vector2.SignedAngle(PaPb, cps[0] - pa);
float PbPaCp1 = Vector2.SignedAngle(PaPb, cps[1] - pa);
Vector2 p1 = Vector2.zero, p2 = Vector2.zero;
if (PbPaCp0 > 0)
{
p1 = cps[0];
p2 = cps[1];
}
else if (PbPaCp1 > 0)
{
p1 = cps[1];
p2 = cps[0];
}
Vector2 paaxis = p2 - pa;
float paang = 2 * Mathf.Abs(PbPaCp0);
//计算外接矩形
int[] paout = GetOutRect(pa, (int)ra);
int left = paout[0];
int right = paout[1];
int bottom = paout[2];
int top = paout[3];
int wid = right - left;
int hei = top - bottom;
int rectlen = wid * hei;
Color[] rectcols = tex2d.GetPixels(left, bottom, wid, hei);
for (int x = left; x < right; x++)
{
for (int y = bottom; y < top; y++)
{
Vector2 xy = new Vector2(x, y);
if (isInSector(xy, pa, ra, paaxis, paang))
{
rectcols[(y - bottom) * wid + (x - left)] = crossColor;
}
}
}
tex2d.SetPixels(left, bottom, wid, hei, rectcols);
}
private bool isInSector(Vector2 xy, Vector2 p, float r, Vector2 axis, float ang)
{
Vector2 xy2p = xy - p;
if ((xy2p.x * xy2p.x + xy2p.y * xy2p.y) <= (r * r))
{
if (AngleNormalize(Vector2.SignedAngle(axis, xy2p)) < ang)
{
return true;
}
return false;
}
return false;
}
private int[] GetOutRect(Vector2 p, int r)
{
int top = (int)p.y + r;
int bottom = (int)p.y - r;
int left = (int)p.x - r;
int right = (int)p.x + r;
top = Mathf.Clamp(top, 0, texHeight - 1);
bottom = Mathf.Clamp(bottom, 0, texHeight - 1);
left = Mathf.Clamp(left, 0, texWidth - 1);
right = Mathf.Clamp(right, 0, texWidth - 1);
return new int[] { left, right, bottom, top };
}
效果如下:
ok,基本上圆形和扇形计算和绘制差不多就这样。