一、概述
本系統(tǒng)擬完成一個(gè)圖形系統(tǒng),對多種常見圖形進(jìn)行基本操作
系統(tǒng)功能
二維圖形的輸入:可輸入或全部清除不同顏色的直線、矩形、圓、橢圓、多邊形、曲線、鉛筆工具
二維圖形的編輯:對于直線、矩形、圓、橢圓、多邊形、曲線,可以通過鼠標(biāo)拖拽標(biāo)出的控制頂點(diǎn)來進(jìn)行編輯
二維圖形的剪裁:可通過拖拽編輯矩形的剪裁窗口對當(dāng)前直線進(jìn)行剪裁
二維圖形的變換:在直線、矩形、圓、橢圓、多邊形、曲線內(nèi)部可通過鼠標(biāo)拖拽進(jìn)行平移,通過按鈕進(jìn)行左右旋轉(zhuǎn)和翻轉(zhuǎn),以及縮放
二維圖形的存儲(chǔ):可將圖形存儲(chǔ)在選擇的路徑下
三維模型的顯示:可選擇加載OFF文件,顯示對應(yīng)的三維模型,并通過鼠標(biāo)拖動(dòng)轉(zhuǎn)換視角
環(huán)境說明
IDE:Qt Creator 4.8.0
Qt版本:Qt 5.4.0 (mingw491_32)
開發(fā)語言:C/C++
Debuggers:GNU gdb 7.8 for MinGW 4.9.1 32bit
二、算法介紹
系統(tǒng)包括的算法主要是二維圖形的操作、三維模型的顯示和用戶操作的響應(yīng)。
2.1 二維圖形
2.1.1 基本圖形
對于各種圖形,雖然其具體的實(shí)現(xiàn)細(xì)節(jié)有所不同(主要體現(xiàn)在圖形的繪制),但它們都有一些公共的屬性和操作,比如計(jì)算圖形的范圍和中心點(diǎn)的位置,以及圖形平移、旋轉(zhuǎn)、縮放操作。這些屬性和方法的算法是相同的。
圖形范圍
對于直線、矩形、圓、橢圓、多邊形、曲線,遍歷其控制點(diǎn)的值,獲得minX、maxX、minY、maxY。上述圖形繪制后形成的所有點(diǎn),不會(huì)有點(diǎn)的x值不在[minX, maxX]或y值不在[minY, maxY]中。四個(gè)值可以確定圖形的范圍,同時(shí)用于邊框的繪制。中心點(diǎn)
中心點(diǎn)位置為(centerX, centerY),其中centerX = (minX + maxX) / 2,centerY = (minY + maxY) / 2。中心點(diǎn)可用做旋轉(zhuǎn)、縮放的基準(zhǔn)點(diǎn)。平移
圖形的繪制依賴于控制點(diǎn),因此只要更新每個(gè)控制點(diǎn)的信息即可。在圖形平移方法中,傳入的參數(shù)為x軸方向移動(dòng)距離的值tx和y軸方向移動(dòng)距離的值ty。對于(x, y)經(jīng)平移(tx, ty)向量后的坐標(biāo)為(x + tx, y + ty)。旋轉(zhuǎn)
圖形旋轉(zhuǎn)傳入的參數(shù)為角度數(shù)θ。控制點(diǎn)(x, y)順時(shí)針旋轉(zhuǎn)θ后,是以中心點(diǎn)(centerX, centerY)為基準(zhǔn)的,坐標(biāo)變?yōu)?centerX + (x - centerX) cosθ - (y - centerY) sinθ, centerY + (x - centerX) sinθ + (y - centerY) cosθ )。縮放
圖形縮放傳入的參數(shù)為在x方向上縮放比例為sx、在y方向上縮放比例為sy。控制點(diǎn)(x, y) 在x方向上縮放比例為sx、在y方向上縮放比例為sy后,坐標(biāo)變?yōu)?x sx + centerX (1 - sx), y sy + centerY (1 - sy))。
2.1.2 直線
對于直線,只需要確定兩個(gè)頂點(diǎn)的坐標(biāo)作為控制點(diǎn)。直線生成有DDA算法、中點(diǎn)畫線算法和Bresenham算法。
- 線生成DDA算法
線生成的DDA算法,主要是在x和y中選擇“變化得快”的方向,在這個(gè)方向上選取等距的一個(gè)個(gè)值,計(jì)算出每個(gè)值對應(yīng)的另一方向的值。介于x和y的值都是整數(shù),因此每次的“增量”至少為1,計(jì)算得到的另一個(gè)值往往不是整數(shù),是需要四舍五入的,同時(shí)后者也可能是不精確的。如果選擇“變化得慢”的方向作為基準(zhǔn),則每次迭代,另一方向上的變化幅度就會(huì)大于1,可以想象這種算法下點(diǎn)會(huì)比較“稀疏”,不如“稠密”的點(diǎn)組成的線精密。
具體來說,當(dāng)直線的斜率(m)的絕對值大于1時(shí),就是所謂的y軸“變化得快”,選取y軸進(jìn)行取樣,當(dāng)斜率絕對值小于1時(shí)則要選取x軸。或者在實(shí)現(xiàn)上,可以直接比較兩個(gè)頂點(diǎn)x軸上和y軸上的差值的絕對值,在差值絕對值較大的方向上取樣,計(jì)算差值較小的方向上的取值。確定方向后可以得到兩個(gè)方向上每次取樣的增量,結(jié)果應(yīng)該是取樣方向上增量為1,計(jì)算方向上增量為[-1, 1]之間的值(絕對值為min( |m|, |1/m|))。
迭代計(jì)算出直線上每個(gè)點(diǎn)的位置。由于c++語言的特性,取值可以是加0.5再取整。部分偽代碼如下:
dx=x2-x1;
dy=y2-y1;
e=(fabs(dx)>fabs(dy))?fabs(dx):fabs(dy);
dx/=e;
dy/=e;
for(int i=1;i<=e;i++)
{
SetPixel((int)(x+0.5), (int)(y+0.5));
x+=dx;
y+=dy;
}
2. 中點(diǎn)畫線算法
根據(jù)直線的兩個(gè)點(diǎn),得到直線的方程f(x, y) = ax + by +c。取樣方向的選擇同DDA算法,下面討論在x軸上取樣、斜率為正的情況,y軸同理。
對于第i個(gè)選擇的像素(xi ,yi),其下一位置的候選像素點(diǎn)為(xi+1, yi)和(xi+1, yi+1),取其中點(diǎn)(xi+1, y+1/2)代入直線方程,計(jì)算出來的決策參數(shù)為d = a * (xi+1) + b * (yi+1/2) +c。當(dāng)d < 0時(shí),中點(diǎn)是在直線的下方的,則靠上位置的(xi+1, yi+1)更靠近直線,如果d <= 0則選取(xi+1, yi)作為下個(gè)像素點(diǎn)。
根據(jù)上述方法進(jìn)行迭代,至到達(dá)頂點(diǎn)。
直線生成的Bresenham算法
Bresenham算法中對于兩個(gè)候選的“下一個(gè)”像素點(diǎn)的選取是與中點(diǎn)畫線算法相同的。△y和△x分別為線段端點(diǎn)的垂直和水平偏離量(整數(shù))在第k步,決策參數(shù)為pk = 2△y ? xk - 2△x ? yk +c,第k+1步的決策參數(shù)為pk+1 = 2△y ? xk+1 - 2△x ? yk+1 +c,根據(jù)決策參數(shù)的符號(hào)來決定像素的選取。直線的裁剪 - Cohen-Sutherland算法
根據(jù)如圖所示的方法對直線段的兩個(gè)端點(diǎn)進(jìn)行編碼,得到兩個(gè)編碼結(jié)果進(jìn)行比較。完全在窗口邊界內(nèi)的線段,兩端點(diǎn)區(qū)域碼均為0000;完全在裁剪矩形外的線段,對兩個(gè)端點(diǎn)區(qū)域碼進(jìn)行邏輯與操作,結(jié)果不為0000;不能確定完全在窗口內(nèi)外的線段,進(jìn)行求交運(yùn)算,求出線段與裁剪框的交點(diǎn)坐標(biāo),作為線段的新端點(diǎn)坐標(biāo)。
2.1.3 矩形
矩形由四個(gè)點(diǎn)首尾相連而成。事實(shí)上,只需要對角線的兩個(gè)控制點(diǎn),就可以組合出四個(gè)頂點(diǎn)的信息。矩形的繪制利用2.1.2中實(shí)現(xiàn)的直線生成算法,繪制出四條直線。
2.1.4 圓
圓可以根據(jù)兩個(gè)控制點(diǎn)形成的矩形來框定,選取了兩個(gè)控制點(diǎn)x軸、y軸上差值較大的一個(gè)作為圓的直徑。圓的函數(shù)為f(x, y) = x^2 + y2^2 - r^2。其畫法可以看作是下述橢圓的特例,不再具體描述。只不過因?yàn)閷ΨQ性,只需要畫出1/8的部分,即只需要畫出區(qū)域1,且區(qū)域1和區(qū)域2的分界點(diǎn)就是x = y的時(shí)候。
2.1.5 橢圓
橢圓可以依靠兩個(gè)控制點(diǎn)形成的一個(gè)矩形來規(guī)定范圍,生成的橢圓內(nèi)切在其中。橢圓的圓心就是中心位置(centerX, centerY),x軸上的直徑ra和y軸上的直徑rb分別是兩點(diǎn)x軸、y軸上差值的。
根據(jù)圖形平移的理論,可以以原點(diǎn)為圓心計(jì)算出橢圓上的點(diǎn)(x, y),再變換成(x + centerX, y + centerY)。因?yàn)闄E圓的對稱性,可以在以原點(diǎn)為圓心的基礎(chǔ)上,只進(jìn)行第一象限的計(jì)算。橢圓的繪制采用中點(diǎn)橢圓生成算法。
中點(diǎn)橢圓生成算法
如圖所示,定義橢圓函數(shù)f(x, y) = ry^2 * x^2 + rx^2 * y^2 - rx^2 * ry^2。以圓切線斜率絕對值為1作為劃分,第一象限可以分為區(qū)域1和區(qū)域2。實(shí)際上,第一象限的切線斜率為負(fù)數(shù),斜率dy/dx = -2ry^2 * x / (rx^2 * y),因此區(qū)域1進(jìn)入?yún)^(qū)域2的條件是2ry^2 * x >= rx^2 * y。區(qū)域1中x軸“變化得快”,在x軸上取樣,區(qū)域2則在y軸上取樣。從(0, ry)或(rx, 0)開始,進(jìn)行迭代,計(jì)算出橢圓上每個(gè)點(diǎn)的位置。下面指出區(qū)域1中的計(jì)算方法,區(qū)域2中同理。
橢圓上的點(diǎn)的決策參數(shù)的初始值為p0 = ry^2 - rx^2 * ry + rx^2 / 4。假設(shè)第k次選擇的像素為(xk, yk),為確定下一次選擇的像素,計(jì)算x(k+1)位置兩個(gè)候選像素點(diǎn)的中點(diǎn),即(xk+1, yk)和(xk+1, yk-1)的中點(diǎn)(xk+1, yk-1/2),將其代入橢圓函數(shù)求出決策參數(shù)。
若p1k < 0,則這個(gè)中點(diǎn)在圓內(nèi),(xk+1, yk)更接近真實(shí)的圓;若p1k >= 0,中點(diǎn)在不在圓內(nèi),選取(xk+1, yk-1)作為下一個(gè)像素點(diǎn)。迭代至循環(huán)結(jié)束,最后將第一象限的每個(gè)像素點(diǎn)映射到其余三個(gè)象限,再從原點(diǎn)將圓心移動(dòng)到原本的中心位置。
2.1.6 多邊形
多邊形的生成也以直線生成為基礎(chǔ),利用直線的繪制將各個(gè)多邊形的頂點(diǎn)作為控制點(diǎn)首尾相連起來。在實(shí)現(xiàn)上,多邊形還需要一個(gè)額外的變量來記錄控制點(diǎn)是否輸入完全(首尾共點(diǎn))。
2.1.7 曲線
曲線的繪制需要若干控制點(diǎn),通過計(jì)算各個(gè)像素點(diǎn)的位置,將所有控制點(diǎn)按照順序平滑地連接起來。曲線分為Bezier曲線和B樣條曲線。
Bezier曲線
給定Pk=(xk,yk,zk),(k=0,1,2,…,n)共n+1個(gè)控制點(diǎn),這些點(diǎn)混合產(chǎn)生位置向量BEZk,n(u)是Bernstein多項(xiàng)式,BEZi,n(u)=C(n,i)ui(1-u)n-i,其中C(n,i)=n!/[i!(n-i)!] (i=0,1,…,n) 。利用Bernstein基函數(shù)的降(升)階公式,可使用遞歸計(jì)算得出Bézier曲線上點(diǎn)的坐標(biāo)位置:BEZk,n(u)=(1-u)BEZk,n-1(u)+uBEZk-1,n-1(u),其中BEZk,k(u)=uk, BEZ0,k(u)=(1-u)k。B樣條曲線
給定n+1個(gè)控制頂點(diǎn){Pi}(i=0,1,…,n),P0P1…Pn為控制多邊形 n+k+2個(gè)參數(shù)節(jié)點(diǎn)向量:Un,k={ui|i=0,1,…,n+k+1,ui≤ui+1}。稱如下形式的參數(shù)曲線P(u)為k+1階(k次)B樣條曲線:其中,Bi,k+1(u)為k+1階(k次)B樣條基函數(shù)。Bi,k+1(u)雙下標(biāo)中下標(biāo)k+1表示k+1階(k次)數(shù)、下標(biāo)i表示序號(hào)。
2.1.8 填充區(qū)域
填充區(qū)域需要先有一個(gè)像素包圍的封閉圖形框架,然后計(jì)算出其內(nèi)的所有像素點(diǎn)的位置。算法有掃描填充圖元生成和區(qū)域填充圖元生成。
掃描填充圖元生成
掃描填充的核心是“掃描”,一般從多邊形的頂點(diǎn)出發(fā),移動(dòng)一條橫越圖形的掃描線,每次求交點(diǎn),從左至右配對存儲(chǔ)這些交點(diǎn)。掃描線每與兩條線相交,這條線的交點(diǎn)表上就會(huì)有添加兩個(gè)交點(diǎn)。需要處理共享頂點(diǎn)兩條邊位于掃描線同側(cè)和異側(cè)的情況。交點(diǎn)表的存儲(chǔ)可以利用活化邊表、排除不必要的求交測試的有序邊表。區(qū)域填充圖元生成
區(qū)域填充需要從一個(gè)連通的圖形內(nèi)的一個(gè)種子點(diǎn)開始,擴(kuò)展到整個(gè)區(qū)域,直至遇到了邊界。該算法比較適合實(shí)現(xiàn)“油漆桶”類似的功能。
由于區(qū)域填充有著“遞歸”的思想,遞歸的結(jié)束條件至關(guān)重要,即對于“遇到邊界”的判斷。在此用到了圖形4連通和8連通的內(nèi)容,對于像素4連通的區(qū)域,其邊界像素只需要是8連通的,也可以是過約束的4連通邊界;而8連通的區(qū)域,邊界像素只能是4連通的,如果邊界是8連通,則欠約束,內(nèi)部的點(diǎn)會(huì)通過對角線位置上沒有填充像素的邊界的空缺,泛濫到邊界之外,造成全局的填充。
此外,像素4連通區(qū)域定義的圖形相對簡單,8連通區(qū)域可以定義更復(fù)雜的圖形,比如兩個(gè)像素間只通過對角線形成連通的圖形。
2.1.9 鉛筆工具
鉛筆工具的控制點(diǎn)是鼠標(biāo)移動(dòng)的軌跡,每個(gè)點(diǎn)都是控制點(diǎn)需要記錄。為了保持軌跡的連貫性,可以在每兩個(gè)距離很小的相鄰點(diǎn)間用直線連接。
2.2 三維模型
取讀OFF文件并顯示其表示的三維模型,重點(diǎn)是在了解OFF文件存儲(chǔ)的格式以及其表示的內(nèi)容。
OFF文件第一行為“off”字符串,第二行是三個(gè)整數(shù),分別表示了這個(gè)模型的點(diǎn)數(shù)量numV、面數(shù)量numF、邊數(shù)量numE。接下來的numV行是點(diǎn)的信息,每行都是每個(gè)點(diǎn)的x、y、z值,值的定義域?yàn)閇-1,1]的浮點(diǎn)數(shù),同時(shí)也確定了點(diǎn)的編號(hào)(從0開始)。然后是numF行的面信息,每行第一個(gè)整數(shù)表示這個(gè)面擁有的點(diǎn)的數(shù)量,接下來的幾個(gè)整數(shù)是點(diǎn)的編號(hào)。
在三維模型中,需要將面打印出來,也就是將每個(gè)面的頂點(diǎn)按順序輸出。
2.3 主界面
主界面是實(shí)現(xiàn)用戶各種操作的接口,主要對于不同的鼠標(biāo)事件進(jìn)行響應(yīng)。
完整的源碼和詳細(xì)的文檔,上傳到了 WRITE-BUG技術(shù)共享平臺(tái) 上,需要的請自取: