數(shù)學篇 cad.net 入門數(shù)學 向量+點乘+叉乘+矩陣+三維旋轉+凸度+反三角函數(shù)+導數(shù)應用+求余
數(shù)學學得好的,都是能把人家的描述語言變成自己的描述語言.
例如你叫這個"矢量積"我叫它"叉積"(叉燒+雞),當你熟悉了之后,再統(tǒng)一回來,你會發(fā)現(xiàn)心情寬闊.
向量(軸)
在編程上面,向量用(X,Y,Z)表示,也就是(123.156,194.156,215,00)
它和點的數(shù)據(jù)結構是一樣的,主要的目的是累加方向和計算夾角.
為什么有向量呢?
因為用點描述一個操作的時候,往往需要進行兩個點+平移.
例如"求夾角",沒有向量的做法是將line的pt1,pt2其中一個點以另一個點平移到line2其中一個點,再進行勾股定理求解.
數(shù)學家們想,這樣麻煩死了,不如它們都平移到(0,0,0),再進行xxxx...
記住一個原則就行:它表示原點(0,0,0)到(X,Y,Z)的方向和長度.
那么兩個點怎么轉換成向量呢?
數(shù)學上就是b點-a點==(x2-x1,y2-y1,z2-z1),而cad提供了一個方法pta.GetVectorTo(ptb)
而單位向量是什么?
我們知道了向量的方向性和大小性,但是方向一樣時候大小可能不一樣.
那我們怎么判斷大小不一樣的時候,方向是不是一樣呢?
這個操作就是"歸一化",也叫單位化.(它可以用來判斷兩根線是不是重疊)
cad是GetNormal,但是我用數(shù)學實現(xiàn)了一下,
看下面點乘中的 public static Point3d GetUnitNormal(this Point3d ob_vector) 了解得知 ??
而法向量是什么?
X軸Y軸形成的平面(XOY面)的法向量就是Z軸.
這有一個非常好的條件,知道一個面(兩向量)可以求一個法向量,知道一個軸可以求軸的面(雖然有無限個).
知道面又有什么用呢?平面切割之后投影可見物啊!
還有的人喜歡把向量和點用標記區(qū)分:(x,y,z,標記)
那么他們運算是豎式計算:
(1,2,3,1) 點
+(4,5,6,0) 向量
---------------
=(5,7,9,1) 點
我擼了一個PointV類更好的說明了4個數(shù)字表達的優(yōu)美,其中涉及了后續(xù)的矩陣齊次坐標等操作.
但是這只是個方法收集類,更優(yōu)美的實現(xiàn)是Array-of-Struct改為Struct-of-Array(SoA),
這方面需要考慮內存布局和CPU緩存機制,可以自行搜索.
點乘的意義
下圖所示,通過向量OA,向量OB,求OP的距離和P點坐標.
現(xiàn)象就是A點投影到OB向量,呈現(xiàn)一個90度直角和P點,而且OA是任意角度的,不需要和x軸重合.
雖然說B站有視頻教程,但是直接看代碼貌似更方便理解.

再深入,通過A,B,C求E點,就是 E == (AC點乘AB).至于為什么要算E點,可以看 數(shù)學篇 cad.net 葛立恒凸包算法和面積最小包圍盒.

再再深入,學習了深度學習之后,
發(fā)現(xiàn)點乘本質上就是加權求和,來獲取兩個特征向量的相關性.
測試命令:
/// <summary>
/// 測試點乘代碼,點1=o,點2=a,點3=b
/// </summary>
[CommandMethod("CmdTest_DotProductOrCross")]
public void CmdTest_DotProductOrCross()
{
var dm = Acap.DocumentManager;
var ed = dm.MdiActiveDocument.Editor;
var ppo = new PromptPointOptions("");
var pts = new List<Point3d>();
for (int i = 0; i < 3; i++)
{
ppo.Message = $"{Environment.NewLine}點位置{i}:";
var pot = ed.GetPoint(ppo);
if (pot.Status != PromptStatus.OK)
return;
pts.Add(pot.Value);
}
var pickPoint = MathHelper.DotProduct(pts[0], pts[1], pts[2]);
ed.WriteMessage($"{Environment.NewLine}點乘P點是:" + pickPoint);
var pickLength = MathHelper.DotProductLength(pts[0], pts[1], pts[2]);
ed.WriteMessage($"{Environment.NewLine}點乘OP長度是:" + pickLength);
var ve1 = new Vector3d(1, 3, 4);
var ve2 = new Vector3d(2, 5, 9);
var ss1 = MathHelper.CrossNormal(ve1, ve2);
ed.WriteMessage($"{Environment.NewLine}叉乘測試****法向量是:" + ss1); //(7,-1,-1)
var aa = MathHelper.CrossAclockwise(pts[0], pts[1], pts[2]);
ed.WriteMessage($"{Environment.NewLine}叉乘測試*****逆時針:" + aa.ToString());
}
點積函數(shù):
/// <summary>
/// 點積,求坐標
/// </summary>
/// <param name="o">原點</param>
/// <param name="a">點</param>
/// <param name="b">點</param>
/// <returns>坐標點</returns>
public static Point3d DotProduct(Point3d o, Point3d a, Point3d b)
{
//點乘就是將oa向量投射到ob向量上面,求得op坐標(也就是呈現(xiàn)90度角的坐標)
var oa = o.GetVectorTo(a);
var obUnit = o.GetVectorTo(b).GetUnitNormal();
var dot = (oa.X * obUnit.X) + (oa.Y * obUnit.Y) + (oa.Z * obUnit.Z);
var p = obUnit * dot;
//如果O不在坐標原點,則需要平移
return new Point3d(p.X + o.X, p.Y + o.Y, p.Z + o.Z);
}
/// <summary>
/// 點積,求值
/// <a > 1.是兩個向量的長度與它們夾角余弦的積 </a>
/// <a href="http://www.rzrgm.cn/JJBox/p/14062009.html#_label1"> 2.求四個點是否矩形使用 </a>
/// </summary>
/// <param name="o">原點</param>
/// <param name="a">點</param>
/// <param name="b">點</param>
/// <returns><![CDATA[>0方向相同,夾角0~90度;=0相互垂直;<0方向相反,夾角90~180度]]></returns>
public double DotProductValue(Point3d o, Point3d a, Point3d b)
{
var oa = o.GetVectorTo(a);
var ob = o.GetVectorTo(b);
return (oa._X * ob._X) + (oa._Y * ob._Y) + (oa._Z * ob._Z);
}
/// <summary>
/// 點積,求長度
/// </summary>
/// <param name="o">原點</param>
/// <param name="a">點</param>
/// <param name="b">點</param>
/// <returns>長度</returns>
public static double DotProductLength(Point3d o, Point3d a, Point3d b)
{
var p = DotProduct(o, a, b);
return o.GetDistanceTo(p);
}
/// <summary>
/// 獲取單位向量,仿照向量的GetNormal的數(shù)學實現(xiàn),了解原理,用了Point3d代替向量
/// </summary>
/// <param name="ob_vector"></param>
/// <returns></returns>
//https://www.bilibili.com/video/BV1qb411M7wL?from=search&seid=9280697047969917119
public static Point3d GetUnitNormal(this Point3d ob_vector)
{
#if true2
var ob_length = Point3d.Origin.GetDistanceTo(ob_vector);
return ob_vector / ob_length;
#else
//兩點的距離,向量模長數(shù)學版 √(x2+y2+z2)
var ob_length = Math.Sqrt(ob_vector.X * ob_vector.X +
ob_vector.Y * ob_vector.Y +
ob_vector.Z * ob_vector.Z);
return new Point3d(ob_vector.X / ob_length, ob_vector.Y / ob_length, ob_vector.Z / ob_length)
#endif
}
叉乘的意義
叉乘有三個意義!
二維叉乘求面積:
A:返回值是數(shù)值,有正負,表示繞原點四象限的位置變換,也就是有向面積,面積屬性可用來求解凸包的單峰函數(shù).

見圖理解一下交叉相乘求面積的數(shù)學知識(就是交叉相乘再相減),下方所有Cross函數(shù)也是基于此制作.
求平行四邊形面積本來就是底乘以高只是換成坐標的求解方式,也可以參考知乎
代碼同下.
二維叉乘求方向:
B:返回值是數(shù)值,有正負,如果>0就是逆時針.
/// <summary>
/// 叉積,二維叉乘計算
/// </summary>
/// <param name="a">傳參是向量,表示原點是0,0</param>
/// <param name="b">傳參是向量,表示原點是0,0</param>
/// <returns>其模為a與b構成的平行四邊形面積</returns>
public static double Cross(Vector2d a, Vector2d b)
{
return a.X * b.Y - a.Y * b.X;
}
/// <summary>
/// 叉積,二維叉乘計算
/// </summary>
/// <param name="o">原點</param>
/// <param name="a">oa向量</param>
/// <param name="b">ob向量,此為判斷點</param>
/// <returns>返回值有正負,表示繞原點四象限的位置變換,也就是有向面積</returns>
public static double Cross(Point2d o, Point2d a, Point2d b)
{
return Cross(o.GetVectorTo(a),o.GetVectorTo(b));
}
/// <summary>
/// 叉積,逆時針方向為真
/// </summary>
/// <param name="o">直線點1</param>
/// <param name="a">直線點2</param>
/// <param name="b">判斷點</param>
/// <returns>b點在oa的逆時針<see cref="true"/></returns>
public static bool CrossAclockwise(Point2d o, Point2d a, Point2d b)
{
return Cross(o, a, b) > -1e-6;//浮點數(shù)容差考慮
}
三維叉乘:
C:返回值是向量.例如(X軸叉乘Y軸==Z軸),如果叉乘的不是XY軸,而是兩個三維向量(并不需要90度),那么就可以求出一個平面的法向量.

同樣叫做A叉乘B,左右手叉乘的方式剛好方向相反.圖來自,必要的視頻參考
左右手坐標系相互轉換:
如果將左a重合到右a,將左b重合到右b,那么左c和右c的關系剛好是正負的關系,結果*-1即可將左c變成右c.
(說明這個僅僅是為了,鏡像導致cad圖元的210組碼(normal)為負數(shù)而做出關聯(lián)修改方式)
三維叉乘(X軸*Y軸==Z軸)
/// <summary>
/// 叉積,求法向量
/// </summary>
/// <param name="a">向量a</param>
/// <param name="b">向量b</param>
/// <returns>右手坐標系系法向量</returns>
public static Vector3d CrossNormal(Vector3d a, Vector3d b)
{
//叉乘:依次用手指蓋住每列,交叉相乘再相減,注意主副順序
//(a.X a.Y a.Z)
//(b.X b.Y b.Z)
return new Vector3d(a.Y * b.Z - b.Y * a.Z, //主-副(左上到右下是主,左下到右上是副)
a.Z * b.X - b.Z * a.X, //副-主
a.X * b.Y - b.X * a.Y); //主-副
}
矩陣乘法
首先要學習一下矩陣乘法,主要掌握相乘再相加.

但是編程應用在于調用方法,只需要了解:
構造一個 xx(平移/旋轉/縮放/透視..)矩陣 * 點 == 變換后的點.
大型矩陣的切割
Lanczos特征值法/PCG法/Multifrontal線性求解法
這些算法的本質是切割大型矩陣,以實現(xiàn)并行計算和硬件加速.
齊次坐標
由于向量乘法需要滿足a矩陣行和b矩陣列個數(shù)相同才能進行矩陣乘法,所以需要引入齊次坐標.
假設使用2x2的矩陣,是沒有辦法描述平移操作的,只有引入3x3矩陣形式,才能統(tǒng)一描述二維中的平移、旋轉、縮放操作. 同理必須使用4x4的矩陣才能統(tǒng)一描述三維的變換.
若不然則可嘗試將平移矩陣的結尾1去掉,你會發(fā)現(xiàn)不滿足矩陣乘法了.
平移矩陣
二維

三維(一般用我)

透視矩陣
這里還有一個好玩的透視矩陣
旋轉矩陣
有了矩陣乘法的思想了,所以應該去看看一些人家寫好的方法了,我這里就沒必要重復敘述了,
他寫得太好了: 旋轉矩陣
然后就可以去隨便找一個c#矩陣代碼,或者使用math.net庫,抄一下跑一下,懂得原理后再抄代碼心里舒服多啦
此處代碼是我做的PointV類中使用到的.
public partial class Transform
{
/// <summary>
/// 旋轉矩陣
/// </summary>
/// <param name="angle">角度</param>
/// <param name="axis">任意旋轉軸</param>
/// <returns></returns>
public static Matrix GetRotateTransform(double angle, PointV axis)
{
angle = -angle;//保證是逆時針
var cos = Math.Cos(angle);
var sin = Math.Sin(angle);
var cosMinus = 1 - cos;
axis = axis.GetUnitNormal();
var u = axis.X;
var v = axis.Y;
var w = axis.Z;
var pOut = new double[4, 4];
{
pOut[0, 0] = cos + u * u * cosMinus;
pOut[0, 1] = u * v * cosMinus + w * sin;
pOut[0, 2] = u * w * cosMinus - v * sin;
pOut[0, 3] = 0;
pOut[1, 0] = u * v * cosMinus - w * sin;
pOut[1, 1] = cos + v * v * cosMinus;
pOut[1, 2] = w * v * cosMinus + u * sin;
pOut[1, 3] = 0;
pOut[2, 0] = u * w * cosMinus + v * sin;
pOut[2, 1] = v * w * cosMinus - u * sin;
pOut[2, 2] = cos + w * w * cosMinus;
pOut[2, 3] = 0;
pOut[3, 0] = 0;
pOut[3, 1] = 0;
pOut[3, 2] = 0;
pOut[3, 3] = 1;
}
return new Matrix(pOut);
}
/// <summary>
/// 應用旋轉矩陣
/// </summary>
/// <param name="pts">點集</param>
/// <param name="rotateMat">旋轉矩陣</param>
/// <returns></returns>
public static PointV[] WarpRotate(PointV[] pts, Matrix rotateMat)
{
#if DEBUG
if (rotateMat.Rows != rotateMat.Cols || rotateMat.Cols != 4)
throw new ArgumentNullException("WarpRotate矩陣大小不對");
#endif
var outPts = new List<PointV>();
foreach (var ptItem in pts)
outPts.Add(SetRotateMat(ptItem, rotateMat));
return outPts.ToArray();
}
/// <summary>
/// 應用旋轉矩陣
/// </summary>
/// <param name="ptItem">點</param>
/// <param name="rotateMat">旋轉矩陣</param>
/// <returns></returns>
public static PointV WarpRotate(PointV ptItem, Matrix rotateMat)
{
#if DEBUG
if (rotateMat.Rows != rotateMat.Cols || rotateMat.Cols != 4)
throw new ArgumentNullException("WarpRotate矩陣大小不對");
#endif
return SetRotateMat(ptItem, rotateMat);
}
static PointV SetRotateMat(PointV ptItem, Matrix rotateMat)
{
var ptRo = rotateMat * new Matrix(ptItem.ToArray());//左乘
return new PointV(ptRo.ToArray());
}
}
/////////////////////////////調用例子//////////////////////////////
/// <summary>
/// 旋轉
/// </summary>
/// <param name="angle">角度</param>
/// <param name="vector">任意轉軸</param>
/// <param name="centerPoint">繞點</param>
/// <returns></returns>
public PointV RotateBy(double angle, PointV vector, PointV centerPoint)
{
var roMat = Transform.GetRotateTransform(angle, vector);
var pt = this - centerPoint;//繞點旁邊平移到原點旁邊
pt = Transform.WarpRotate(pt, roMat);
return pt + centerPoint;//原點旁邊平移到繞點旁邊
}
二維旋轉
如果不寫成矩陣形式,寫成函數(shù)形式呢?貌似更方便入門和理解.
我發(fā)現(xiàn)人家的圖不是很好,自己做了一個.

public static Point2d Rotation(Point2d pt, double al)
{
var xa = pt.X;
var ya = pt.Y;
var cos = Math.Cos(al);
var sin = Math.Sin(al);
var xb = xa * cos + ya * sin;
var yb = ya * cos - xa * sin;
return new Point2d(xb, yb);
}
有了二維旋轉之后,你就能知道點是如何在原點上圍繞Z軸旋轉了.
因為Z軸在XOY面上有無數(shù)個,所以必須繞原點,否則平移到原點再繞,最后再平移回去.
而二維擴展到三維,無非就是點Z的位置是0,其他軸旋轉就是其他軸的點是0,
這樣就可以明白了XYZ軸的旋轉了,而任意軸旋轉看下面的三維旋轉.
三維旋轉
那么三維的點怎么從用戶坐標系到世界坐標系?或者世界轉用戶?
首先我們要知道這個點所在的坐標系(用戶坐標系,物體坐標系,相對坐標系),
拿到這個坐標系的Z軸(提及過的任意軸)
- 重合用戶原點到世界原點(就是求向量,向量就是原點開始)
- 重合用戶Z軸到世界Z軸,使得剩下世界Z軸可以旋轉(不就可以套用了二維旋轉了嗎).
- 記錄下XYZ三個軸的轉動角度.
變換必然會依照順序,出現(xiàn)XYZ按照夾角alx,aly,alz轉動,
逆向則為ZYX按照夾角-alz,-aly,-alx轉動.
這樣即可變換兩個坐標系. - 平移回去用戶原點.
步驟2就是核心:
重合Z軸只需要旋轉XY兩個軸即可,如圖所示: 原點和P點形成了一個向量(我習慣稱作用戶z軸)

2.1. 把P點投影到YOZ面.獲取oq與世界Z軸的夾角alx.
2.2. 通過alx旋轉世界X軸后,P點將會變換為r點,r點在XOZ平面上.獲取or與世界Z軸角度aly.
2.3. 接著獲取alz,看代碼就曉得了.
public static partial class MathHelper {
/// 原理見:http://www.rzrgm.cn/graphics/archive/2012/08/10/2627458.html
/// 以及:http://www.doc88.com/p-786491590188.html
/// <summary>
/// 輸入一個法向量(用戶Z軸),獲取它與世界坐標X軸和Y軸的夾角,
/// 旋轉這兩個夾角可以重合世界坐標的Z軸
/// </summary>
/// <param name="userZxis">用戶坐標系的Z軸</param>
/// <param name="alx">輸出X軸夾角</param>
/// <param name="aly">輸出Y軸夾角</param>
public static void ToWcsAngles(this Vector3d userZxis, out double alx, out double aly)
{
//處理精度
double X = Math.Abs(userZxis.X) < 1e-10 ? 0 : userZxis.X;
double Y = Math.Abs(userZxis.Y) < 1e-10 ? 0 : userZxis.Y;
double Z = Math.Abs(userZxis.Z) < 1e-10 ? 0 : userZxis.Z;
//YOZ面==旋轉X軸..投影的
var oq = Point3d.Origin.GetVectorTo(new Point3d(0, Y, Z));
alx = Vector3d.ZAxis.GetAngleTo(oq);
alx = oq.Y > 0 ? alx : Pi2 - alx;
alx = Math.Abs(Pi2 - alx) < 1e-10 ? 0 : alx;
//XOZ面==旋轉Y軸..旋轉的
var userZ = Math.Pow(Y * Y + Z * Z, 0.5);
var or = Point3d.Origin.GetVectorTo(new Point3d(X, 0, userZ));
aly = Vector3d.ZAxis.GetAngleTo(or);
aly = or.X < 0 ? aly : Pi2 - aly;
aly = Math.Abs(Pi2 - aly) < 1e-10 ? 0 : aly;
}
/// <summary>
/// X軸到向量的弧度,cad的獲取的弧度是1PI,所以轉換為2PI(上小,下大)
/// </summary>
/// <param name="ve">向量</param>
/// <returns>弧度</returns>
public static double GetAngle2XAxis(this Vector2d ve)
{
double alz = Vector2d.XAxis.GetAngleTo(ve);//觀察方向不要設置
alz = ve.Y > 0 ? alz : Pi2 - alz; //逆時針為正,如果-負值控制正反
alz = Math.Abs(Pi2 - alz) < 1e-10 ? 0 : alz;
return alz;
}
/// <summary>
/// X軸到向量的弧度,cad的獲取的弧度是1PI,所以轉換為2PI(上小,下大)
/// </summary>
public static double GetAngle2XAxis(this Point2d startPoint, Point2d endtPoint)
{
return startPoint.GetVectorTo(endtPoint).GetAngle2XAxis();
}
/// <summary>
/// 三個軸角度
/// 重合兩個坐標系的Z軸,求出三個軸旋轉角度,后續(xù)利用旋轉角度正負和次序來變換用戶和世界坐標系
/// </summary>
/// <param name="ucs">用戶坐標系</param>
/// <param name="alx">坐標系間的X軸旋轉角度</param>
/// <param name="aly">坐標系間的Y軸旋轉角度</param>
/// <param name="alz">坐標系間的Z軸旋轉角度</param>
public static void ToWcsAngles(this CoordinateSystem3d ucs, out double alx, out double aly, out double alz)
{
//ucs可能帶有新原點,設置到0,世界坐標系原點重合
var ucs2o = new CoordinateSystem3d(Point3d.Origin, ucs.Xaxis, ucs.Yaxis);
//XY軸通過叉乘求得Z軸,但是這個類幫我求了
ucs2o.Zaxis.ToWcsAngles(out alx, out aly);
//使用戶X軸與世界XOY面共面,求出Z軸旋轉角
var newXa = ucs2o.Xaxis.RotateBy(alx, Vector3d.XAxis)
.RotateBy(aly, Vector3d.YAxis);
alz = -newXa.GetAngle2XAxis();
}
private static Point3d Wcs2Ucs(this Point3d pt, CoordinateSystem3d ucs)
{
ucs.ToWcsAngles(out double alx, out double aly, out double alz);
pt = new Point3d(pt.X - ucs.Origin.X,
pt.Y - ucs.Origin.Y,
pt.Z - ucs.Origin.Z);
pt = pt.RotateBy(alx, Vector3d.XAxis, Point3d.Origin)
.RotateBy(aly, Vector3d.YAxis, Point3d.Origin)
.RotateBy(alz, Vector3d.ZAxis, Point3d.Origin);
return pt;
}
private static Point3d Ucs2Wcs(this Point3d pt, CoordinateSystem3d ucs)
{
ucs.ToWcsAngles(out double alx, out double aly, out double alz);
//把pt直接放在世界坐標系上,此時無任何重合.
//進行逆旋轉,此時向量之間重合.
pt = pt.RotateBy(-alz, Vector3d.ZAxis, Point3d.Origin)
.RotateBy(-aly, Vector3d.YAxis, Point3d.Origin)
.RotateBy(-alx, Vector3d.XAxis, Point3d.Origin);
//平移之后就是點和點重合,此點為世界坐標系.
pt = new Point3d(pt.X + ucs.Origin.X,
pt.Y + ucs.Origin.Y,
pt.Z + ucs.Origin.Z);
return pt;
}
/// <summary>
/// 把一個點從一個坐標系統(tǒng)變換到另外一個坐標系統(tǒng)
/// </summary>
/// <param name="userPt">來源點</param>
/// <param name="source">來源坐標系</param>
/// <param name="target">目標坐標系</param>
/// <returns>目標坐標系的點</returns>
public static Point3d Transform(this Point3d userPt, CoordinateSystem3d source, CoordinateSystem3d target)
{
//世界坐標是獨特的
var wcs = Matrix3d.Identity.CoordinateSystem3d;
Point3d pt = Point3d.Origin;
if (Equals(source, target))
pt = userPt;
//al的角度是一樣的,旋轉方式取決于正負號
else if (!Equals(source, wcs) && !Equals(target, wcs))//用戶轉用戶
pt = userPt.Ucs2Wcs(source).Wcs2Ucs(target);
if (Equals(target, wcs))
pt = userPt.Ucs2Wcs(source);
else if (!Equals(target, wcs))
pt = userPt.Wcs2Ucs(target);
return pt;
}
/// <summary>
/// 獲取坐標系統(tǒng)三維
/// </summary>
/// <param name="ed"></param>
/// <param name="wcs"></param>
/// <param name="ucs"></param>
public static void GetWcsUcs(this Editor ed, out CoordinateSystem3d wcs, out CoordinateSystem3d ucs)
{
Matrix3d Ide_wcs = Matrix3d.Identity;//獲取世界坐標系
wcs = Ide_wcs.CoordinateSystem3d;
Matrix3d used_ucs = ed.CurrentUserCoordinateSystem;//當前用戶坐標系
ucs = used_ucs.CoordinateSystem3d;
}
}
羅德里格公式
眾所周知,Z軸的旋轉是從X軸開始的,并且逆時針為正.
逆時針好懂,因為是右手坐標系決定旋轉為正的方向.
但是為什么是X軸呢?其實很簡單,是基于自然約定.
有多自然呢?下面是一個3*3的單位矩陣是這樣的:
1,0,0
0,1,0
0,0,1
可以看見這個斜三角的美.
單位矩陣延伸出三個基礎旋轉矩陣,
即為繞X旋轉矩陣,繞Y旋轉矩陣,繞Z旋轉矩陣,
然后就又回到了X軸了.
那么三維任意軸旋轉的話,按理說有無數(shù)根起始軸啊?
此時又是怎么確定起始軸呢?
我第一次遇到這個問題是在復現(xiàn)al命令的時候,
因為一根線可能是三維的,不是簡單的二維ro就行,
但是為什么桌子能夠每次都吻合一模一樣呢?
讓我們問問AI,
得到的信息是直接通過 羅德里格公式.
羅德里格公式,它本質上就是Z軸旋轉矩陣的一種泛化,
它需要兩個參數(shù),
一個是轉軸,一個是角度,得到一個旋轉矩陣.
也就是你可以先很笨的認為,
可以從任意軸重合到Z軸,
再實現(xiàn)依照Z軸旋轉矩陣,最后逆轉回去.
不過由于太笨了...羅德里格帶著它的公式走來了.
不過,在Acad上面寫個啥矩陣,肯定有函數(shù)已經(jīng)寫了.
// 將世界坐標轉換為視圖坐標
// 這個世界有兩種人,一種人會把角度改為負數(shù),一種人會改轉軸方向
public static Matrix3d WorldToEye(this AbstractViewTableRecord view) =>
Matrix3d.WorldToPlane(view.ViewDirection) *
Matrix3d.Displacement(view.Target.GetAsVector().Negate()) *
Matrix3d.Rotation(view.ViewTwist, view.ViewDirection, view.Target);
LU分解/QR分解/SVD分解.
SVD分解是最好的,
是因為SVD分解適用于所有矩陣,
而LU分解僅適用于方陣,
QR分解僅使用于滿秩矩陣(也就是主元不為0)
https://www.zhihu.com/question/22572629
凸度
參考
mac-lee,它已經(jīng)說得很好了
bulgeconversion-參考鏈接1
bulgeconversion-參考鏈接2
已知圓弧的起點端點和凸度計算圓心 防丟鏈接
說明
A: 此函數(shù)將把用于定義Acad圓弧轉多邊形弧段(頂點,凸度).
B: 還可以從 凸度==0 判斷點是否在直線上.
返回:凸度可能是正的,也可能是負的,這取決于通過這三個點的弧線是順時針路徑還是逆時針.
為了盡量避免歧義,在交流的時候不要使用中點和終點,改為腰點和尾點.

在cad的Polyline屬性有pl.GetBulgeAt(i)自帶的.
Lisp的處理方式:
3-Points to Bulge縮寫是 3P to B..感覺怪怪吼!
;; 3-Points to Bulge - Lee Mac
(defun LM:3p->bulge ( pt1 pt2 pt3 )
((lambda ( a ) (/ (sin a) (cos a))) (/ (+ (- pi (angle pt2 pt1)) (angle pt2 pt3)) 2))
)
C#的處理方式
/// <summary>
/// 凸度的測試命令,點1=圓弧頭點,點2=圓弧腰點,點3=圓弧尾點
/// </summary>
[CommandMethod("test_BulgeCenter")]
public void test_BulgeCenter()
{
var dm = Acap.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
var ppo = new PromptPointOptions("");
var pts = new List<Point3d>();
for (int i = 0; i < 3; i++)
{
ppo.Message = $"{Environment.NewLine}點位置{i}:";
var pot = ed.GetPoint(ppo);
if (pot.Status != PromptStatus.OK)
return;
pts.Add(pot.Value);
}
var getBulge = MathHelper.GetArcBulge(pts[0], pts[1], pts[2]);
var center = MathHelper.GetArcBulgeCenter(pts[0].ToPoint2d(), pts[2].ToPoint2d(), getBulge);
ed.WriteMessage($"{Environment.NewLine}凸度是:{getBulge}");
ed.WriteMessage($"{Environment.NewLine}圓心點是:" + center.DebugString());
var arcLength = MathHelper.GetArcLength(pts[0].ToPoint2d(), pts[2].ToPoint2d(), getBulge);
ed.WriteMessage($"{Environment.NewLine}弧長是:{arcLength}");
}
計算凸度
/// http://www.lee-mac.com/bulgeconversion.html
/// <summary>
/// 求凸度,判斷三點是否一條直線上
/// </summary>
/// <param name="arc1">圓弧起點</param>
/// <param name="arc2">圓弧腰點</param>
/// <param name="arc3">圓弧尾點</param>
/// <returns>逆時針為正,順時針為負</returns>
public static double GetArcBulge(Point2d arc1, Point2d arc2, Point2d arc3)
{
double dStartAngle = GetAngle2XAxis(arc2, arc1);
double dEndAngle = GetAngle2XAxis(arc2, arc3);
//求的P1P2與P1P3夾角
var talAngle = (Math.PI - dStartAngle + dEndAngle) / 2;
//凸度==拱高/半弦長==拱高比值/半弦長比值
//有了比值就不需要拿到拱高值和半弦長值了,因為接下來是相除得凸度
double bulge = Math.Sin(talAngle) / Math.Cos(talAngle);
//處理精度
if (bulge > 0.9999 && bulge < 1.0001)
bulge = 1;
else if (bulge < -0.9999 && bulge > -1.0001)
bulge = -1;
else if (Math.Abs(bulge) < 1e-10)
bulge = 0;
return bulge;
}
/// <summary>
/// 求凸度,判斷三點是否一條直線上..慢一點
/// </summary>
/// <param name="arc1">圓弧起點</param>
/// <param name="arc2">圓弧腰點</param>
/// <param name="arc3">圓弧尾點</param>
/// <returns>逆時針為正,順時針為負</returns>
public static double GetArcBulge(this Arc arc)
{
var arc1 = arc.StartPoint;
var arc2 = arc.GetPointAtDist(arc.GetDistAtPoint(arc.EndPoint) / 2);//圓弧的腰點
var arc3 = arc.EndPoint;
return GetArcBulge(arc1, arc2, arc3);
}
/// <summary>
/// 求凸度,判斷三點是否一條直線上
/// </summary>
/// <param name="arc" >圓弧</ param>
/// <returns></returns>
public static double GetArcBulge2(this Arc arc)
{
//還有一種求凸度方法是tan(圓心角 / 4),但是丟失了方向,
//再用叉乘來判斷腰點方向,從而凸度是否 * -1
double bulge = Math.Tan(arc.TotalAngle / 4); //凸度是圓心角的四分之一的正切
Point3d midpt = arc.GetPointAtDist(arc.GetDistAtPoint(arc.EndPoint) / 2);//圓弧的腰點
Vector3d vmid = midpt - arc.StartPoint; //起點到腰點的向量
Vector3d vend = arc.EndPoint - arc.StartPoint; //起點到尾點的向量
Vector3d vcross = vmid.CrossProduct(vend); //叉乘求正負
//根據(jù)右手定則,腰點向量在尾點向量右側,則叉乘向量Z值為正,圓弧為逆時針
if (vcross.Z < 0)
bulge *= -1;
//處理精度
if (bulge > 0.9999 && bulge < 1.0001)
bulge = 1;
else if (bulge < -0.9999 && bulge > -1.0001)
bulge = -1;
else if (Math.Abs(bulge) < 1e-10)
bulge = 0;
return bulge;
}
/// <summary>
/// 測試凸度函數(shù)哪個快
/// </summary>
[CommandMethod(nameof(CmdTest_GetArcBulge))]
public void CmdTest_GetArcBulge() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
var arc = new Arc(Point3d.Origin, 20, 0, Math.PI / 2);
TimeHelper.RunTime(() => {
for (int i = 0; i < 500_0000; i++)
{
MathHelper.GetArcBulge(arc);
}
}, "A測試時間是:");
TimeHelper.RunTime(() => {
for (int i = 0; i < 500_0000; i++)
{
MathHelper.GetArcBulge2(arc);
}
}, "B測試時間是:");
}
/// <summary>
/// 測試運行時間
/// </summary>
/// <param name="action"></param>
public static long RunTime(Action action, string str = null) {
var stopwatch = new Stopwatch();
stopwatch.Start(); //開始監(jiān)視
action.Invoke(); //執(zhí)行委托
stopwatch.Stop(); //停止監(jiān)視
// stopwatch.Restart();//把前面的時間清空
var timespan = stopwatch.ElapsedMilliseconds; //毫秒數(shù)
//TimeSpan timespan = stopwatch.Elapsed; //獲取當前實例測量得出的總時間
if (str != null)
Debug.WriteLine(str + timespan.ToString() + "毫秒");
return timespan;
}
凸度求圓心
/// http://bbs.xdcad.net/thread-722387-1-1.html
/// <summary>
/// 凸度求圓心
/// </summary>
/// <param name="arc1">圓弧頭點</param>
/// <param name="arc3">圓弧尾點</param>
/// <param name="bulge">凸度</param>
/// <returns>圓心</returns>
public static Point2d GetArcBulgeCenter(Point2d arc1, Point2d arc3, double bulge)
{
if (bulge == 0)
throw new ArgumentNullException("凸度為0,此線是平的");
var x1 = arc1.X;
var y1 = arc1.Y;
var x2 = arc3.X;
var y2 = arc3.Y;
var b = (1 / bulge - bulge) / 2;
var x = (x1 + x2 - b * (y2 - y1)) / 2;
var y = (y1 + y2 + b * (x2 - x1)) / 2;
return new Point2d(x, y);
}
凸度求弧長

/// <summary>
/// 凸度求弧長
/// </summary>
/// <param name="arc1">圓弧頭點</param>
/// <param name="arc3">圓弧尾點</param>
/// <param name="bulge">凸度</param>
/// <returns></returns>
public static double GetLength(Point2d arc1, Point2d arc3, double bulge)
{
var bowLength = arc1.DistanceTo(arc3); //弦長
var bowLength2 = bowLength / 2; //半弦
var archHeight = Math.Abs(bulge) * bowLength2; //拱高==凸度*半弦
//根據(jù)三角函數(shù): (弦長/2)2+(半徑-拱高)2=半徑2
//再根據(jù):完全平方公式變形: (a+b)2=a2+2ab+b2、(a-b)2=a2-2ab+b2
var r = (bowLength2 * bowLength2 + archHeight * archHeight) / (2 * archHeight); //半徑
//求圓心角:一半圓心角[(對邊==半弦長,斜邊==半徑)]; *2就是完整圓心角
var asin = Math.Asin(bowLength2 / r) * 2; //反正弦
//弧長公式: 弧長=絕對值(圓心弧度)*半徑...就是單位*比例..縮放過程
var arcLength = asin * r;
return arcLength;
}
凸度求圓弧腰點
/// <summary>
/// 圓弧的腰點
/// </summary>
/// <param name="arc1">圓弧點1</param>
/// <param name="arc3">圓弧點3</param>
/// <param name="bulge">凸度</param>
/// <returns>返回腰點</returns>
/// <exception cref="ArgumentNullException"></exception>
public static Point2d GetArcMidPoint(Point2d arc1, Point2d arc3, double bulge)
{
if (bulge == 0)
throw new ArgumentException("凸度為0,此線是平的");
var center = GetArcBulgeCenter(arc1, arc3, bulge);
var angle1 = center.GetVectorTo(arc1).GetAngle2XAxis();
var angle3 = center.GetVectorTo(arc3).GetAngle2XAxis();
// 利用邊點進行旋轉,就得到腰點,旋轉角/2
// 需要注意鏡像的多段線
double angle = angle3 - angle1;
if (bulge > 0)
{
if (angle < 0)
angle += Math.PI * 2;
}
else
{
if (angle > 0)
angle += Math.PI * 2;
}
return arc1.RotateBy(angle / 2, center);
}
多邊形面積(不支持圓弧)
// 計算多邊形面積的方法,使用鞋帶公式
// 點集要順序,可以自交
public static double CalculateArea(List<Point3d> points) {
int n = points.Count;
double area = 0.0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
area += points[i].X * points[j].Y;
area -= points[i].Y * points[j].X;
}
return Math.Abs(area) / 2.0;
}
反三角函數(shù)
由于我經(jīng)常忘記三角函數(shù)和反三角函數(shù)之間的關系...敲了一點cad的測試命令.

[CommandMethod("test_InverseTrigonometric")]
public void test_InverseTrigonometric()
{
var doc = Acap.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
ed.WriteMessage($"{Environment.NewLine}反三角函數(shù)");
var ppo = new PromptPointOptions("")
{
AllowNone = false,
};
var pts = new List<Point3d>();
for (int i = 0; i < 3; i++)
{
ppo.Message = $"{Environment.NewLine}選擇三角形角點{i}";
var gp = ed.GetPoint(ppo);
if (gp.Status != PromptStatus.OK)
return;
pts.Add(gp.Value);
}
//由于某些實際情況缺少一條邊,此時需要求夾角,則使用反三角函數(shù)
var 鄰邊 = pts[0].DistanceTo(pts[1]);
var 對邊 = pts[1].DistanceTo(pts[2]);
var 斜邊 = pts[2].DistanceTo(pts[0]);
//Math.Cos(angle) == 鄰邊 / 斜邊;
//Math.Sin(angle) == 對邊 / 斜邊;
//Math.Tan(angle) == 對邊 / 鄰邊;
var acos = Math.Acos(鄰邊 / 斜邊); //反余弦
var asin = Math.Asin(對邊 / 斜邊); //反正弦
var atan = Math.Atan(對邊 / 鄰邊); //反正切
var atan2 = Math.Atan2(56, 30) * 180 / Math.PI;//(Y,X)與(0,0 X軸)的夾角
ed.WriteMessage($"{Environment.NewLine}角度a是:{asin},{atan},{acos}");
}
它們的實現(xiàn)方式是泰勒展開
高精度反三角函數(shù)的實現(xiàn)
導數(shù)應用(光線反射)

計算方式是:
一階導數(shù)y'=dy/dx=df(x)/dx
二階導數(shù)y''=dy'/dx=[d(dy/dx)]/dx=d2y/dx2=d2f(x)/dx2
...
看一點偽代碼,形象的理解一下:
//一階導數(shù)
double GetFirstDerivative(double dy)
{
double dx = 1e-10;//趨近于0
return dy / dx;
}
double 二階導數(shù)(double dy)
{
return GetFirstDerivative(GetFirstDerivative(dy));
}
double 三階導數(shù)(double dy)
{
return GetFirstDerivative(GetFirstDerivative(GetFirstDerivative(dy)));
}
//四階五階六階...無限嵌套就是無限階...
cad實現(xiàn)
起初我根據(jù)群友認為的二階導數(shù)就是法線,實際上求了一個特殊解:只有圓弧的二階導數(shù)等于法線.
而要擴展到曲線的法線,則通用解是切線旋轉90度.
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
using System.Collections.Generic;
using System;
namespace JoinBox
{
public partial class CmdTest
{
[CommandMethod("test_Derivative")]
public void test_Derivative()
{
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
//兩點確定射線入射角
var pts = new List<Point3d>();
var ppo = new PromptPointOptions("");
for (int i = 0; i < 2; i++)
{
ppo.Message = $"{Environment.NewLine}測試導數(shù),點位置{i + 1}:";
var pot = ed.GetPoint(ppo);
if (pot.Status != PromptStatus.OK)
return;
pts.Add(pot.Value);
}
var ray0 = new Ray
{
BasePoint = pts[0],
SecondPoint = pts[1],
};
db.Action(tr => {
//獲取屏幕內的圖元,再獲取射中的
var ids = SelectViewportObjectId(ed, tr);
if (ids == null)
return;
foreach (var id in ids)
{
var ent = id.ToEntity(tr);
if (ent is Curve cur)
{
if (ent is not Arc)//測試時候過濾掉無用注釋用
continue;
var ptc = new Point3dCollection();
ray0.IntersectWith(cur, Intersect.OnBothOperands, ptc.Collection, (int)IntPtr.Zero, (int)IntPtr.Zero); //得出的所有交點
if (ptc.Count == 0)
continue;
var intPt = ptc[0]; //交點可能是3d點
// 一階導數(shù)是切線
Vector3d yt1 = cur.GetFirstDerivative(intPt);
var ray1 = new Ray
{
BasePoint = intPt,
SecondPoint = new Point3d(yt1.X + intPt.X, yt1.Y + intPt.Y, 0),
};
ray1.ColorIndex = 1;
tr.AddEntityToMsPs(db, ray1);
#if true3
// 只有圓弧的二階導數(shù)是法線
Vector3d yt2 = cur.GetSecondDerivative(intPt);
var ray2 = new Ray
{
BasePoint = intPt,
SecondPoint = new Point3d(yt2.X + intPt.X, yt2.Y + intPt.Y, 0),
};
#else
// 曲線的法線通用解是:切線是旋轉90度
var ray2 = ray1.Clone() as Ray;
ray2.EntityRotate(Math.PI / 2, Vector3d.ZAxis, intPt);
#endif
ray2.ColorIndex = 2;
tr.AddEntityToMsPs(db, ray2);
// 射出光線
var ray3 = new Ray
{
BasePoint = intPt,
SecondPoint = pts[0],
};
ray3.EntityMirror(ray2.BasePoint, ray2.SecondPoint, out Entity ray3mr);
ray3mr.ColorIndex = 3;
tr.AddEntityToMsPs(db, ray3mr);
}
}
});
}
/// <summary>
/// 獲取當前屏幕的所有圖元
/// </summary>
/// <param name="ed"></param>
/// <param name="tr"></param>
/// <returns></returns>
ObjectId[] SelectViewportObjectId(Editor ed, Transaction tr)
{
var view = ed.GetCurrentView();
var cpt = view.CenterPoint;
var w = view.Width / 2;
var h = view.Height / 2;
//先忽略視口原點角度等信息
var min = new Point3d(cpt.X - w, cpt.Y - h, 0);
var max = new Point3d(cpt.X + w, cpt.Y + h, 0);
var ssget = ed.SelectCrossingWindow(min, max);
if (ssget.Status != PromptStatus.OK)
return null;
return ssget.Value.GetObjectIds();
}
}
}
public static partial class EntityEdit
{
// 鏡像
public static Matrix3d EntityMirror(this Entity ent,
Point3d point1, Point3d point2, out Entity entity)
{
//計算變換矩陣
Matrix3d mt = Matrix3d.Mirroring(new Line3d(point1, point2));//鏡像矩陣
entity = ent.GetTransformedCopy(mt); //這里可能要替換成克隆
return mt;
}
}
public static partial class EntityAdd
{
/// <summary>
/// 將圖形添加到數(shù)據(jù)庫的當前空間中
/// </summary>
/// <param name="db">圖形數(shù)據(jù)庫</param>
/// <param name="ent">圖形對象</param>
/// <returns>圖形的ObjectId</returns>
public static ObjectId AddEntityToMsPs(this Transaction tr, Database db, Entity ent)
{
ObjectId entId;
//在位編輯的時候自動加到塊內了,而不是模型空間,因為會把臨時的數(shù)據(jù)拷貝回去塊表記錄上面
//出現(xiàn)eLockViolation 是因 CommandFlags.Session | CommandFlags.Redraw 又沒有鎖文檔
var btRec = tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord;
//加圖元到塊表記錄,設定返回值
entId = btRec.AppendEntity(ent);
//更新數(shù)據(jù)
tr.AddNewlyCreatedDBObject(ent, true);
btRec.DowngradeOpen();
btRec.Dispose();
return entId;
}
}
子函數(shù):
本博客引用: 委托db.Action cad.net 委托無返回值寫法
本博客引用: ptc.Collection 二度封裝Point3dCollection
二階導數(shù)
那曲線的二階導數(shù)有什么用呢?

它表達斜率的變化率,所以越凸的地方值越大.
看圖上綠色的部分,長度越長表示值越大,表示你油門踩得越深,然后也表達加速度了.
所以運動控制上面有用.
而對橢圓用二階導數(shù),會得到橢圓中心點,而不是兩個焦點.
橢圓實際上是任意軸觀察的圓形,這樣就求出圓心了.

三階導數(shù)
它是變化率的變化率,它有什么用呢?
找拐點
相關閱讀
取余(取模)
9%2=>1
9/2=4.5
則用4*2=8;商整數(shù)部分*除數(shù)
再用9-8=1;結果就是余數(shù)
2%40=>2
2/40=0.05
則用0*40=0;商整數(shù)部分*除數(shù)
再用2-0=2;結果就是余數(shù)
但是除數(shù)為2的冪次方,減少使用除法器(就是減少牛頓迭代),實現(xiàn)加速
即:
a & (2^n - 1)
它可以做什么?
A: 如果有一個一維數(shù)組,你需要按照每4個分一份,
那么就可以循環(huán)的時候 i%4==0 每次new List Add(i1,i2,i3,i4)從而實現(xiàn)切割.
B: 哈希值%3就是將數(shù)據(jù)大致均勻的分到三分之一,看得出%3就是在[0,2]閉區(qū)間:
0%3==0
1%3==1
2%3==2
3%3==0
100%3==1
C: 分庫分表
一致性哈希算法
快速模運算
public static ulong ModPow(ulong baseValue, ulong exponent, ulong modulus)
{
// 初始化結果為1,因為任何數(shù)的0次方都是1
ulong result = 1;
// 底數(shù)對模數(shù)取模,以防底數(shù)過大
baseValue %= modulus;
// 當指數(shù)不為0時,繼續(xù)循環(huán)
while (exponent > 0)
{
// 如果當前指數(shù)是奇數(shù),將底數(shù)乘到結果上,并取模
if ((exponent & 1) == 1)
result = (result * baseValue) % modulus;
// 底數(shù)平方并取模
baseValue = (baseValue * baseValue) % modulus;
// 指數(shù)右移一位(等同于除以2)
exponent >>= 1;
}
// 返回結果
return result;
}
(完)
浙公網(wǎng)安備 33010602011771號