開始的嘮叨html
說是3D引擎確實有點過於博眼球了,其實就是實現了一個透視投影,固然也不是那麼簡單的。 此篇文章是純粹給小白看的 高手請勿噴 。也稱之爲小向帶你圖形學入門基礎 。 哇哈哈哈哈 一說到作一個3D畫面的東東 一說老是到DirectX OpenGL 這些玩意兒 咱們這些菜鳥老是 想到哇擦擦 哇C++的 哇 計算機圖形學好難。這玩意兒難度好大。其實就那麼回事兒 ,DirectX OpenGL 只是工具 而已, 只要把原理搞懂了 你看我用low逼的GDI照樣給你繪製一個3D物體 能夠這樣說即便不用GDI 其餘任何能劃線 畫點的東西 ,我在安卓平臺上照樣給你實現這個效果。不要侷限於工具 誰說作3D就只能用DirectX OpenGL了 ,誰說作3D只能用C++了 。c++
頂點數據展示算法
首先是3D編程中通用的數據展示 那就是頂點組成的網格數據 稱之爲mesh, 3個點爲一組 組成的三角面片數據。三個點順時針的方向那麼 箭頭方向爲外表面 另外一面爲內表面 ,在繪製的時候 內表面不可見。
編程
好那麼咱們也以此方式來定義數據 ,咱們定義的東西是一個 中心位置在( 0,0,-130)處的立方體。咱們的觀察點在(0, 0, 0)處 正對着立方體觀察。 因爲咱們想讓立方體一個面的顏色相同,因此是兩個爲一組定義的 ,固然三角形也是以一樣的 兩個爲一組 組成一個正方形面。由此對物體表面空間點的描述數據就作好了。
好 ,定義數據的代碼:c#
1 public class Marsh 2 { 3 public List<Point3dF> points; 4 public Marsh() 5 { 6 points = new List<Point3dF>(); 7 8 //0 9 Point3dF pointA = new Point3dF(30, 30, -160); 10 //1 11 Point3dF pointB = new Point3dF(-30, 30, -160); 12 //2 13 Point3dF pointC = new Point3dF(-30, -30, -160); 14 //3 15 Point3dF pointD = new Point3dF(30, -30, -160); 16 //4 17 Point3dF pointE = new Point3dF(30, 30, -100); 18 //5 19 Point3dF pointF = new Point3dF(-30, 30, -100); 20 //6 21 Point3dF pointG = new Point3dF(-30, -30, -100); 22 //7 23 Point3dF pointH = new Point3dF(30, -30, -100); 24 25 points.Add(pointA); 26 points.Add(pointB); 27 points.Add(pointC); 28 points.Add(pointD); 29 30 points.Add(pointE); 31 points.Add(pointF); 32 points.Add(pointG); 33 points.Add(pointH); 34 35 36 37 path1 = new List<int>() { 38 4, 6,7, 39 4,5,6, 40 5 ,2 ,6, 41 5, 1, 2, 42 1 ,3 ,2, 43 1, 0 ,3, 44 0 ,7 ,3, 45 0 ,4, 7 46 , 47 4, 1, 5, 48 4 ,0 ,1, 49 6, 2 ,7, 50 2, 3, 7 51 }; 52 53 faceColors = new List<Brush>(); 54 Random rdm = new Random(); 55 56 for (int i = 0; i < 6; i++) 57 { 58 Brush b= new SolidBrush(Color.FromArgb(rdm.Next(0, 255), rdm.Next(0, 255), rdm.Next(0, 255))); 59 //Brush b = new SolidBrush(Color.FromArgb(266 / 6 * i, 266 / 6 * i, 266 / 6 * i)); 60 faceColors.Add(b); 61 faceColors.Add(b); 62 } 63 64 } 65 public List<Brush> faceColors; 66 public List<int> path1; 67 68 }
關於透視投影 和頂點繪製數組
我數學底子差 在寫這個例子以前參考了不少前輩的 圖形學理論基礎。最主要是透視投影 和3D旋轉矩陣繞任意軸旋轉 。甚至沒徹底搞懂 因而我就抄起代碼開搞了。 不得不說這玩意兒真的頗有意思。
首先是透視投影:http://blog.csdn.net/popy007/article/details/1797121
做者講的很詳細 其實我只看到一半 ,後面矩陣推導那些太難了 沒有繼續往下啃 。視線是一個發散的方式從一個點出去 (其實最後發現不用管什麼視椎體不視椎體的)。 假設視點前面有一張半透明的紙張 視線上的點是怎麼打到紙張上的?關於這個問題 你要粗暴點確實很簡單 就是三角形 初中的知識。
dom
就如開始所述 視點在(0,0,0) 處看向 位於(0,0,-130) 的立方體 ,假設有一架攝像機 ,那麼上圖就是他的從空中看下去的俯視圖。設p爲(x,z) p'爲(x' ,z') 。則x'=-N(x/z) y'=-N(y/z)。爲了方便 咱們的數據定義也是跟示意圖上差很少的。因而咱們依葫蘆畫瓢 把全部的點繪製出來 包裝成一個paint函數。
須要注意的是平面座標系 跟屏幕座標之間的轉換 ,其實不難 你其餘計算應用數學公式 數學函數 仍是同樣該咋算咋算。 完成後咱們平面座標系的0,0 對應屏幕座標的0,0 。
看到沒 x軸0左邊也是負數 不用管 就只是y的符號不同 變成-y就能夠了。而後要讓他顯示在窗口中間 還要進行偏移 就是x加偏移, y加偏移 就這樣就完成啦 。 哈哈哈哈哈。 函數
1 public void paint() 2 { 3 Graphics gph = Graphics.FromHwnd(this.Handle); 4 gph.Clear(Color.Lavender); 5 //進行到屏幕座標的映射(x y z) 6 //p~ =(-n x/z -n y/z -n) 7 PointF screenLastPoint= PointF.Empty; 8 for (int i = 0; i < msh.path1.Count / 3; i++) 9 { 10 //if (i >= 4) 11 // return; 12 PointF screenPointA = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3]].x / msh.points[msh.path1[i * 3]].z)) , (float)((-nearPlan) * (msh.points[msh.path1[i * 3]].y / msh.points[msh.path1[i * 3]].z)) ); 13 PointF screenPointB = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 1]].x / msh.points[msh.path1[i * 3 + 1]].z)) , (float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 1]].y / msh.points[msh.path1[i * 3 + 1]].z)) ); 14 PointF screenPointC = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 2]].x / msh.points[msh.path1[i * 3 + 2]].z)) , (float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 2]].y / msh.points[msh.path1[i * 3 + 2]].z)) ); 15 16 screenPointA.Y = -screenPointA.Y; 17 screenPointB.Y = -screenPointB.Y; 18 screenPointC.Y = -screenPointC.Y; 19 20 screenPointA.Y=screenPointA.Y+offsety; 21 screenPointB.Y= screenPointB.Y+offsety; 22 screenPointC.Y = screenPointC.Y + offsety; 23 24 screenPointA.X = screenPointA.X + offsetx; 25 screenPointB.X = screenPointB.X + offsetx; 26 screenPointC.X = screenPointC.X + offsetx; 27 28 System.Drawing.Drawing2D.GraphicsPath ph = new System.Drawing.Drawing2D.GraphicsPath( 29 new PointF[] { screenPointA, screenPointB, screenPointC }, 30 new byte[] { 1, 1, 1 }, 31 System.Drawing.Drawing2D.FillMode.Winding); 32 33 34 //---求法向量及夾角 若是爲true 則渲染面//計算當前管線三角面片的法向量 是否朝着鏡頭 ,最終決定是否可見 35 if (angelCalc(msh.points[msh.path1[i * 3]], msh.points[msh.path1[i * 3+1]], msh.points[msh.path1[i * 3+2]]) == true) 36 gph.FillPath(msh.faceColors[i], ph); 37 } 38 39 //繪製邊框 40 gph.DrawLine(Pens.Red, new PointF(offsetx - 36, offsety - 36), new PointF(offsetx + 36, offsety - 36)); 41 gph.DrawLine(Pens.Red, new PointF(offsetx + 36, offsety - 36),new PointF(offsetx + 36, offsety + 36)); 42 gph.DrawLine(Pens.Red, new PointF(offsetx + 36, offsety +36), new PointF(offsetx - 36, offsety + 36)); 43 gph.DrawLine(Pens.Red, new PointF(offsetx - 36, offsety + 36), new PointF(offsetx - 36, offsety - 36)); 44 45 ////繪製網格線 46 //screenLastPoint = PointF.Empty; 47 //for (int i = 0; i < msh.path1.Count / 3; i++) 48 //{ 49 // //if (i >= 4) 50 // // return; 51 // PointF screenPointA = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3]].x / msh.points[msh.path1[i * 3]].z)), (float)((-nearPlan) * (msh.points[msh.path1[i * 3]].y / msh.points[msh.path1[i * 3]].z))); 52 // PointF screenPointB = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 1]].x / msh.points[msh.path1[i * 3 + 1]].z)), (float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 1]].y / msh.points[msh.path1[i * 3 + 1]].z))); 53 // PointF screenPointC = new PointF((float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 2]].x / msh.points[msh.path1[i * 3 + 2]].z)), (float)((-nearPlan) * (msh.points[msh.path1[i * 3 + 2]].y / msh.points[msh.path1[i * 3 + 2]].z))); 54 55 // screenPointA.Y = -screenPointA.Y; 56 // screenPointB.Y = -screenPointB.Y; 57 // screenPointC.Y = -screenPointC.Y; 58 59 // screenPointA.Y = screenPointA.Y + offsety; 60 // screenPointB.Y = screenPointB.Y + offsety; 61 // screenPointC.Y = screenPointC.Y + offsety; 62 63 // screenPointA.X = screenPointA.X + offsetx; 64 // screenPointB.X = screenPointB.X + offsetx; 65 // screenPointC.X = screenPointC.X + offsetx; 66 67 // gph.DrawLine(Pens.Red, screenPointA, screenPointB); 68 // gph.DrawLine(Pens.Red, screenPointB, screenPointC); 69 // gph.DrawLine(Pens.Red, screenPointC, screenPointA); 70 71 //} 72 73 }
繞着座標軸進行旋轉 工具
最開始我沒有繪製面只是繪製的頂點線框而已 。而後我想作的是旋轉 讓他轉起來,總共八個點連成線就是立方體了,哪怕是low逼的線條 只要轉起來是否是就有立方體的樣子了。 哇哈哈哈哈。最開始我想的很簡單啊 立體的旋轉也沒啥不得了的啊 ,好比饒y軸旋轉 我把他當成平面的不就得了麼 y不變x和z變。 繞x軸旋轉 同理。 我原來也寫過平面的點進行旋轉的計算。 爲了符合圖形學上的標準方式 最後我仍是使用二維矩陣旋轉的方式: 學習
1 public void RotationTest2() 2 { 3 4 //二維空間旋轉矩陣爲 : x是角度 5 //cos(x) -sin(x) (1-cos(x))tx+ty*sin(x)) x 6 //Sin(x) cos(x) (1-cos(x))ty-tx*sin(x)) y 7 8 //2pi 等於360度 9 //繞y軸旋轉 10 //double xita = ((Math.PI * 2d) / 360d) * 2d; 11 double xita = ((Math.PI * 2d) / 360d) * anglex; 12 double cosx = Math.Cos(xita); 13 double sinx = Math.Sin(xita); 14 15 double xitay = ((Math.PI * 2d) / 360) * angley; 16 double cosy = Math.Cos(xitay); 17 double siny = Math.Sin(xitay); 18 19 for (int i = 0; i < msh.points.Count; i++) 20 { 21 //Point3dF tmpPoint = new Point3dF(msh.points[i].x, msh.points[i].y, msh.points[i].z); 22 Point3dF tmpPoint = new Point3dF(mshSource.points[i].x, mshSource.points[i].y, mshSource.points[i].z); 23 msh.points[i].x = 24 tmpPoint.x * cosx + ((-sinx) * tmpPoint.z) + 25 (((1d - cosx) * 0d) + ((-130d) * sinx)); 26 27 msh.points[i].z = 28 tmpPoint.x * sinx + (cosx * tmpPoint.z) + 29 (((1d - cosx) * (-130d)) - ((0d) * sinx)); 30 31 msh.points[i].y = tmpPoint.y; 32 33 //--------------------------------- 34 tmpPoint = new Point3dF(msh.points[i].x, msh.points[i].y, msh.points[i].z); 35 36 msh.points[i].y = tmpPoint.y * cosy + ((-siny) * tmpPoint.z) + 37 (((1d - cosy) * 0d) + ((-130d) * siny)); 38 39 msh.points[i].z = tmpPoint.y * siny + (cosy * tmpPoint.z) + 40 (((1d - cosy) * (-130d)) - ((0d) * siny)); 41 } 42 43 }
注意了 繞着任意軸進行旋轉
如今我想作的是作一個跟蹤球效果 。鼠標按下拖動的時候讓物體 像烤肉串樣繞着一根軸旋轉。 網上跟蹤球都是旋轉相機 咱們這裏直接旋轉物體座標。繞着任意軸旋轉啊繞着任意軸旋轉的矩陣 說實話 3D旋轉矩陣這個我搞不懂 ,我看不懂推導過程 可是我會看公式 哇哈哈哈哈。
https://www.cnblogs.com/graphics/archive/2012/08/10/2627458.html
其餘的博文裏貼出來的旋轉矩陣也是這樣 直接把他的代碼抄下來之 ,c++的 我抄成c#的 沒啥難的 我已經超過好些c++代碼了。總之咱們要作的就是 得出一個二維數組做爲矩陣回傳 讓全部座標根據此矩陣進行運算。注意有兩個基本概念: 兩個點相減 a-b 得出的是 b到a 的向量 (0,0) -(1,1) =(-1,-1) ,而後是向量歸一化: 什麼叫歸一化, 就是 把向量的方向不變 長度變到單位長度 ,也就是1。問向量歸一化怎麼搞 。好 ,好比一個二維向量,計算原理就是經過距離計算公式得出距離。這個距離與1的比值等於 現x與歸一化後x的比值:求歸一化後y的值同理。固然這些都是基礎的沒什麼特別說的。
求旋轉矩陣的函數:
1 //獲得旋轉矩陣 2 double[,] RotateArbitraryLine(Point3dF v1, Point3dF v2, double theta) 3 { 4 5 double a = v1.x; 6 double b = v1.y; 7 double c = v1.z; 8 Point3dF p = new Point3dF(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z); 9 //v2歸一化 10 double x_p2 = p.x / Math.Sqrt(Math.Pow(p.x, 2) + Math.Pow(p.y, 2)); 11 double y_p2 = p.y / Math.Sqrt(Math.Pow(p.x, 2) + Math.Pow(p.y, 2)); 12 13 if (double.IsNaN(x_p2)) 14 x_p2 = 0; 15 if (double.IsNaN(y_p2)) 16 y_p2 = 0; 17 18 double u = x_p2; 19 double v = y_p2; 20 double w = 0d;// -130d;// 0d; 21 22 double uu = u * u; 23 double uv = u * v; 24 double uw = u * w; 25 double vv = v * v; 26 double vw = v * w; 27 double ww = w * w; 28 double au = a * u; 29 double av = a * v; 30 double aw = a * w; 31 double bu = b * u; 32 double bv = b * v; 33 double bw = b * w; 34 double cu = c * u; 35 double cv = c * v; 36 double cw = c * w; 37 38 double costheta = Math.Cos(theta); 39 double sintheta = Math.Sin(theta) ; 40 double[,] pOut = new double[4, 4]; 41 pOut[0,0] = uu + (vv + ww) * costheta; 42 pOut[1,0] = uv * (1 - costheta) + w * sintheta; 43 pOut[2,0] = uw * (1 - costheta) - v * sintheta; 44 pOut[3,0] = 0; 45 46 pOut[0,1] = uv * (1 - costheta) - w * sintheta; 47 pOut[1,1] = vv + (uu + ww) * costheta; 48 pOut[2,1] = vw * (1 - costheta) + u * sintheta; 49 pOut[3,1] = 0; 50 51 pOut[0,2] = uw * (1 - costheta) + v * sintheta; 52 pOut[1,2] = vw * (1 - costheta) - u * sintheta; 53 pOut[2,2] = ww + (uu + vv) * costheta; 54 pOut[3,2] = 0; 55 56 pOut[0,3] = (a * (vv + ww) - u * (bv + cw)) * (1 - costheta) + (bw - cv) * sintheta; 57 pOut[1,3] = (b * (uu + ww) - v * (au + cw)) * (1 - costheta) + (cu - aw) * sintheta; 58 pOut[2,3] = (c * (uu + vv) - w * (au + bv)) * (1 - costheta) + (av - bu) * sintheta; 59 pOut[3,3] = 1; 60 61 return pOut; 62 }
函數寫好了 矩陣也能得出了,還有個問題:函數的那幾個參數 ,旋轉的度數也好搞 按下的時候記錄一個點 拖動的時候計算跟他的距離 距離做爲度數 拖動50像素 旋轉50度。 旋轉的那根兒軸你怎麼得出來,開始點容易 立方體的中心是0 0 -130 。按下的時候記錄了按下開始點 鼠標的移動就已是一個向量了 因此咱們只須要對這個向量 繞z軸進行90度旋轉 ,z也設成-130 就跟中心點平齊了 就是須要的烤肉串兒的旋轉軸了。 好 原理講完了 ,代碼走起:
1 private void Form1_MouseMove(object sender, MouseEventArgs e) 2 { 3 //必需要得出旋轉的軸才行 4 if (pressed) 5 { 6 7 //中心點 0,0,-130 8 //經過拖動遠近決定旋轉角度,垂直向量 獲得旋轉軸 9 10 //得出鼠標拖動向量 11 12 Point3dF dragJuli = new Point3dF((e.Location.X - startPoint.X),( e.Location.Y - startPoint.Y),-130d); 13 //還要旋轉90度纔是真正的旋轉軸 14 //cos(x) -sin(x) 15 //Sin(x) cos(x) 16 double cos90=0d; 17 double sin90=1d; 18 var x = dragJuli.x * 0d + dragJuli.y * 1d; 19 var y = dragJuli.x * 1d + dragJuli.y * 0d; 20 21 Point3dF dragJuli2 = new Point3dF(x, y, dragJuli.z); 22 23 //Point3dF dragJuli90= 24 //x1 x2+y1 y2=0 25 //double x2=(-dragJuli.y)/(dragJuli.x); 26 //垂直的旋轉軸向量 27 //Point3dF roll = new Point3dF(x2, 1, -130); 28 29 //拖動距離 拖動距離等於角度 30 angelourua= Math.Sqrt(Math.Pow((e.Location.X - startPoint.X), 2) + Math.Pow((e.Location.Y - startPoint.Y), 2)); 31 angelourua = angelourua % 360; 32 angelourua = ((Math.PI * 2d) / 360d) * angelourua; 33 34 double[,] roatMatarix= RotateArbitraryLine(new Point3dF(0, 0, -130d), dragJuli2,angelourua); 35 36 RotationTest(roatMatarix); 37 paint(); 38 39 } 40 }
好就這樣 先進行3d空間的點旋轉, 再進行平面座標映射繪製 用線連起來。是否是就有點3D立體的樣子了。哇哈哈哈哈
注意了 面繪製
前面的都是有些挖的別人的,這個功能絕對是本身搗鼓出來的。開始那些不管是兩兩之間的線條繪製啥的都只能算是 點繪製 ,咱們如今要進行面繪製。首先你要清楚的是個人頂點三角面片數據已經給出了, 3d座標點打在半透明紙張上 的x,y 也已經得出了。 調用gdi的fillpath按ABC 的順序連起來 就能繪製一個三角面片 是否是很簡單。可是先別慌 還有兩個問題須要處理,一個就是可見面判別。 就是兩個三角面片 的位置 決定了 在透視投影的時候哪一個在前哪一個在後, 還有面相交的狀況呢 ? 是否是很複雜?其實根本不用管,即便要管 只要你使用z緩衝算法 也不是很複雜 zbuffer 。就是在求出屏幕x y事後把同xy的點z越靠近視點的放在前面 這樣就達到目的了。這裏咱們先 無論這個zbuffer算法 下次有空閒了咱們再來寫。這裏咱們使用另一種方式 經過判別正向面與後向面來達到目的。前面咱們不是說了嗎 :
咱們經過計算每一個三角面片的法向量, 而後咱們有一個視點到三角面片的向量 ,經過計算兩向量的點積 而後經過反餘弦函數就能夠得出兩向量的夾角 。 若是夾角大於90度表明三角面片正向面朝着視點, 若是小於90度表明正向面背對了視點, 則不對這個三角面片進行渲染。如此一來 你仔細想一想 咱們的立方體至始至終不會存在一個面把另外一個面遮擋的狀況。
好了原理講至此 好下面 根據原理擼代碼:
1 //平面是否面向攝像機的判別 2 public bool angelCalc( Point3dF A,Point3dF B,Point3dF C) 3 { 4 //https://zhidao.baidu.com/question/810216091258785532.html 5 //AB、AC所在平面的法向量即AB×AC=(a,b,c),其中: 6 //a=(y2-y1)(z3-z1)-(z2-z1)(y3-y1) 7 //b=(z2-z1)(x3-x1)-(z3-z1)(x2-x1) 8 //c=(x2-x1)(y3-y1)-(x3-x1)(y2-y1) 9 10 //先得出點 對應的向量 11 //Point3dF AB = new Point3dF(B.x - A.x, B.y - A.y, B.z - A.z); 12 13 //首先求出法向量 14 double a = ((B.y - A.y)*(C.z - A.z) - (B.z - A.z)*(C.y - A.y)); 15 double b = (B.z - A.z) * (C.x - A.x) - (C.z - A.z) * (B.x - A.x); 16 double c = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y); 17 Point3dF bb = new Point3dF(a, b, c); 18 //套公式 第二冊 下b 39 經過兩向量的cos函數 繼而經過反餘弦得出角度 19 var angelPlan =Math.Acos( 20 (A.x * bb.x + A.y * bb.y + A.z * bb.z) / ( 21 Math.Sqrt(Math.Pow(A.x, 2) + Math.Pow(A.y, 2) + Math.Pow(A.z, 2)) * 22 Math.Sqrt(Math.Pow(bb.x, 2) + Math.Pow(bb.y, 2) + Math.Pow(bb.z, 2)) 23 ) 24 ); 25 26 if (angelPlan > (Math.PI / 2))//法向量與鏡頭的夾角大於90度 表明三角面片面向攝像機 則可見 27 return true; 28 else//不然不可見 29 return false; 30 }
關於光照
光照這玩意兒仍是 用到三角面片的法向量 ,三角面片正對着光 則表面亮度最高 ,垂直則變成黑暗。參照面繪製的原理就能夠搞出來 我這裏就每一個面各用些五光十色的顏色算了吧 懶得整了。
其餘的
還有個人攝像機鏡頭是固定的 ,其實還有不少工做須要作 。 但願各位大大繼續完善。看下效果 是否是有模有樣:上個gif圖:
你能夠把平面面向攝像機判別函數返回的值反向一下看看什麼效果,是否是看到內壁的那一面了 是否是很神奇哇哈哈哈哈是的你沒看錯就這麼幾百行核心部分就實現了 差很少都是數學知識 ,其餘的都是添磚加瓦的事,原理都在這了。 好了 之後再看DirectX 啊OpenGL啊 圖形學 變換矩陣啊那些的 別被唬住了 沒那麼難的。按說的話這些知識整體來講蠻難的 可是畢竟仍是學太高中的向量 兩向量垂直時點積等於零 這些之類的 , 靠着摸着石頭過河 把這些半懂不懂的知識 拼湊起來 加上各類度娘 和查資料 來實現 沒想到居然成功了。 意外意外 ,固然學習是要靠本身的 你想直接度娘給你出來個你要的效果 那是不可能的。