
1. 項目概述為什么2024年還要聊一個60年前的畫線算法先說明一下這篇文章的主角是顯示底層的東西——Bresenham’s Algorithm。如果你做過嵌入式GUI、寫過單片機屏幕驅動、搞過游戲引擎的渲染管線或者只是用Python畫過幾條線你大概率已經用過它只是沒認出來。它是計算機圖形學里最經典的直線光柵化算法1962年由Jack Bresenham在IBM提出目的只有一個在像素點陣上快速畫出盡可能接近理想直線的點序列。它的核心貢獻聽起來簡單但在那個CPU主頻按KHz算的年代是革命性的只用整數加減法和移位判斷就能完成直線繪制完全不用浮點運算、不用乘法、不用開方。放到今天這個優勢在PC上已經不新鮮了但在MCU、FPGA、老式街機硬件、以及任何需要以極低成本繪制線條的場景里它依然是首選方案。這篇文章會把這條“線”從數學原理一路拆到工程落地——推導過程、各象限處理、圓和橢圓的擴展、性能實測還有在實際項目中踩過的坑。適合三類人看剛接觸圖形學、想徹底搞懂光柵化原理的學生在嵌入式設備上做GUI、需要高效繪制基本圖形的開發者以及寫渲染器時想優化draw_line性能的工程師。我盡量用口語講但該上公式的地方不會含糊。提示本文所有代碼示例均為教學用途可直接復制到本地編譯運行。環境建議使用C語言或任何你熟悉的語言不需要額外依賴圖形庫核心驗證用控制臺打印字符即可完成。2. 光柵化的本質為什么畫一條直線沒有想象中簡單在理解Bresenham之前得先搞清楚一個底層矛盾數學上的直線是連續的、無限細的而屏幕上的像素是一個個方格只能整體點亮或熄滅。把連續的東西離散化到網格上這個行為在圖形學里叫光柵化Rasterization。2.1 像素網格與直線的“真面目”假設你有一個分辨率為20x15的屏幕想在它上面畫一條從(1,1)到(18,11)的線段。數學上這條線可以寫成y 1 (10/17) * (x - 1) ≈ 0.588x 0.412如果你天真地用這個方程x從1取到18把每個x對應的y四舍五入后畫點會得到什么結果看起來是一條“馬馬虎虎”的線但細看會發現兩個問題第一效率低。每次迭代都要計算浮點乘法0.588x還要做取整運算。在1962年的機器上浮點乘法可能比加法慢幾十倍而顯示一幀可能就要畫幾百條線累計開銷非常可觀。第二鋸齒形狀不穩定。浮點誤差累積到一定量級后同樣的斜率在不同的x位置像素點的走向可能不一致視覺上出現“忽粗忽細”或者“階梯分布不均勻”的情況。這里的核心問題是用浮點方式畫線每一步都在做“近似”但每一步的近似誤差沒有被顯式跟蹤和管理。Bresenham的高明之處在于它把“誤差”變成了一個可以進行整數比較的狀態變量用極小的代價維持光柵化結果在整個線段長度上的最優性。2.2 樸素方法的性能瓶頸為了讓你更直觀地感受性能差距我給一個實測數據。在一顆主頻72MHz的STM32F103單片機上不做任何優化用浮點直線法畫一條800x480屏幕對角線大約940個點每次循環執行一次浮點乘法、一次浮點加法、一次浮點轉整數整體耗時大約是2.1毫秒。這還沒算調庫、內存訪問和循環開銷。如果換成Bresenham整數算法同樣的畫線過程耗時約為0.3毫秒。差距接近7倍。對于逐幀渲染的動畫場景這個差距直接決定了幀率是否達標。更重要的是Bresenham使用的整數運算在無FPU的Cortex-M0上也能高效執行而浮點法在這種芯片上壓根跑不動。這就是為什么直到今天幾乎所有低端圖形庫如U8g2、LVGL的底層、Adafruit GFX在drawLine函數里實現的都是Bresenham或其變種。它不只是一個“老古董”而是經過工程驗證的最優解之一。3. 核心原理解析誤差項如何驅動像素選擇這一節是全文的重點。我會從直覺類比講到數學推導保證不跳步。理解了這一節的誤差項邏輯后面所有代碼都是水到渠成。3.1 用“天平”類比理解誤差累積想象你在走一條狹窄的臺階路臺階寬1米、每級高0.5米你的目標是從起點走到終點且盡量沿著一條“想象中的斜線”走。你每向右跨1米理論上應該上升0.5米但臺階只能一級一級上不能上半個臺階。于是你記一個“誤差賬本”每跨一步你欠了0.5米的高度當欠賬累計到需要上升一整級時你就上一級臺階同時清零一部分欠賬。這就是Bresenham決策參數decision parameter的直覺來源它不是直接算y值而是維護一個“誤差天平”每次x遞增時天平向一側偏轉當天平偏轉超過某個閾值就在y方向移動一格并修正誤差項。3.2 從斜率到決策參數的數學推導現在來嚴格的。以下推導針對第一象限、斜率在0到1之間的線段這是最基礎的情況其他情況后文會說明。設線段起點為(x0, y0)終點為(x1, y1)滿足dx x1 - x0 0dy y1 - y0 0且 dy dx。直線方程是y (dy / dx) * x B當x每增加1時y理論上的增量是 dy/dx這是一個0到1之間的分數。由于像素的y坐標必須是整數所以每一步只有兩個選擇當前位置(xk, yk)下一個點的候選位置(xk1, yk) 或 (xk1, yk1)關鍵是決定選哪一個。Bresenham定義了一個誤差項當前實際直線在x xk1處的真實y坐標與像素點yk的距離差為d_upper (yk 1) - y_real d_lower y_real - yk其中 y_real (dy / dx) * (xk 1) B。比較d_upper和d_lower的大小如果d_lower d_upper說明真實線更接近yk而不是yk1所以垂直方向不動否則y方向要加1。把兩個距離相減得到決策值d_lower - d_upper [y_real - yk] - [(yk 1) - y_real] 2 * (y_real - yk) - 1把y_real代入并乘以dxdx恒正不影響符號判斷只為了消去分母p_k dx * (d_lower - d_upper) 2 * dy * (xk 1) 2 * dx * B - 2 * dx * yk - dx這里含有BB跟起點有關。我們來消除它。對于起點(x0, y0)有y0 (dy / dx) * x0 B所以 dx * B y0 * dx - dy * x0代回去得到p_k 2 * dy * (xk 1) 2 * (y0 * dx - dy * x0) - 2 * dx * yk - dx 2 * dy * (xk - x0) - 2 * dx * (yk - y0) 2 * dy - dx這個式子告訴我們可以用增量方式迭代。關鍵是求出p_k和p_{k1}之間的關系當p_k 0時選擇 (xk1, yk)則p_{k1} p_k 2 * dy當p_k 0時選擇 (xk1, yk1)則p_{k1} p_k 2 * (dy - dx)初始值p_0在(x0, y0)處計算p_0 2 * dy - dx到這里全部推導結束。整個算法每次迭代只需要做比較p_k的符號加一個預先算好的常數值2dy 或 2(dy-dx)有時y坐標加1一次循環兩個加法一個比較零乘法零浮點零除法。3.3 為什么這個算法是最優的可能有人會問“四舍五入不也是近似嗎Bresenham的四舍五入有什么特殊”區別在于普通四舍五入是每步獨立的它不考慮之前幾步累積的誤差。舉個例子如果某幾步真實線都恰好落在兩個像素的正中間四舍五入會全部向上取整導致畫出的線明顯偏高。Bresenham的決策參數是“記憶性”的p_k的累積天然決定了哪些步該進位、哪些步不該進位從全局上看它會保證整條線段上的像素點與實際直線之間的垂直距離在任何位置都不會超過0.5個像素——這是光柵化理論里能達到的最優逼近。這個“不超過0.5像素誤差”的結論不是玄學它是從決策參數的構造方式直接推出的。我用數學歸納法驗證過假設第k步選擇的是離真實線最近的像素那么第k1步的決策規則恰好就是在兩個候選像素中選擇離真實線更近的那個。每步都是局部最優且局部最優的組合不會造成誤差的線性累積于是全局也是最優。4. 工程實現要點從基礎函數到全象限覆蓋原理懂了代碼就好寫了。但寫代碼時有一堆細節要處理比如參數校驗、dx和dy的正負、以及算法如何擴展到任意方向的直線。這一節直接給出完整實現并解釋每個關鍵決策。4.1 基礎版本第一象限0-1斜率實現先看最基礎的版本只支持dx dy 0的情況void draw_line_basic(int x0, int y0, int x1, int y1) { int dx x1 - x0; int dy y1 - y0; int p 2 * dy - dx; int x, y y0; for (x x0; x x1; x) { set_pixel(x, y); if (p 0) { p 2 * dy; } else { y; p 2 * (dy - dx); } } }這個函數在限定條件下是正確的但真實項目里幾乎沒人直接用。因為線段的方向千變萬化斜率為負、斜率大于1、從右往左畫、縱坐標從下往上畫……每一種情況都需要單獨處理。用if硬分四個象限可以但代碼會膨脹。更優雅的做法是統一走增量對稱的通用版本。4.2 全象限通用版本統一坐標變換這里用到一個經典思路先把計算過程約束到“第一象限的0-1斜率模式”再通過坐標變換映射回真實方向。具體分為兩步第一步確定主軸。在Bresenham算法里x和y地位并不對稱需要拿變化量大的軸作為“步進軸”每輪必加1變化量小的軸作為“移動軸”偶爾加1或減1。若abs(dx) abs(dy)主軸是x否則主軸是y。第二步確定方向。為每個軸定義步長因子如果終點坐標大于起點步長因子為1否則為-1。這樣就把“從右往左”的線段鏡像成“從左往右”來迭代最后映射回實際方向。一個經過實際項目檢驗的實現如下void draw_line(int x0, int y0, int x1, int y1) { int dx abs(x1 - x0); int dy abs(y1 - y0); int sx (x0 x1) ? 1 : -1; int sy (y0 y1) ? 1 : -1; int err dx - dy; int e2; while (1) { set_pixel(x0, y0); if (x0 x1 y0 y1) break; e2 2 * err; if (e2 -dy) { // 等效于 err -dy主軸向x方向步進 err - dy; x0 sx; } if (e2 dx) { // 等效于 err dx副軸向y方向步進 err dx; y0 sy; } } }這個實現的巧妙之處在于它把所有情況統一成了一個對稱結構err初始為dx - dy每輪迭代主軸和副軸方向都可能更新甚至有可能x和y同時更新對應斜率恰好為1時。這就是維基百科上那段著名代碼的原理也是實際庫里最常見的版本。我看過LVGL和Adafruit的實現核心邏輯都是這個結構只是變量命名和邊界條件略有區別。4.3 圓和橢圓的快速擴展Bresenham的誤差思想不僅能畫直線還能畫圓。畫圓的思路是利用八分對稱性只算第一象限中從(0, r)到(r/√2, r/√2)的45度圓弧其余部分通過對稱映射生成。圓版的決策參數推導類似最終迭代式為f(x, y) x2 y2 - r2在第k步位于(xk, yk)確定下一步是選(xk1, yk)還是(xk1, yk-1)決策參數為p_k 2*(xk1)2 yk2 (yk-1)2 - 2*r2這個式子單獨看很丑但是同樣可以用增量方式簡化。實際操作中我推薦另一種更直觀的寫法——利用中點圓算法Midpoint Circle Algorithm的變體代碼更短且同樣全整數void draw_circle(int xc, int yc, int r) { int x 0, y r; int d 1 - r; // 初始決策參數 while (x y) { set_pixel(xc x, yc y); set_pixel(xc - x, yc y); set_pixel(xc x, yc - y); set_pixel(xc - x, yc - y); set_pixel(xc y, yc x); set_pixel(xc - y, yc x); set_pixel(xc y, yc - x); set_pixel(xc - y, yc - x); x; if (d 0) { d 2 * x 1; } else { y--; d 2 * (x - y) 1; } } }這段代碼在r比較大時畫出的圓非常光滑而且無浮點運算。如果你要畫橢圓有兩種取向一種是仿射變換先畫圓再把坐標不等比縮放但會導致線寬不均另一種是用一般式的橢圓Bresenham——它需要維護兩個誤差項因為長軸和短軸的曲率不同。工程上我更推薦前者因為對于絕大多數GUI場景橢圓只是少量裝飾元素縮放帶來的輕微不均肉眼看不出但代碼復雜度大幅降低。5. 實戰優化與體系化注意事項算法本身講完了但“能跑”和“跑得好”是兩碼事。這一節匯總我自己在不同硬件和場景下用Bresenham時踩過的坑以及一些常規教程不會寫清楚的經驗。5.1 性能實測整數算法到底能快多少我做過一個對比實驗環境是樹莓派PicoRP2040133MHzCortex-M0屏幕是128x64的SSD1306 OLED通過I2C接口輸出。分別用浮點DDA和整數Bresenham繪制同一組100條隨機線段每次繪制前清屏重復100次取平均。結果方法單條線段平均耗時100條線段總耗時浮點DDA約0.82ms約82ms整數Bresenham約0.36ms約36ms查找表Bresenham核心循環展開約0.30ms約30ms注意這里的耗時包含了送屏的I2C傳輸純計算時間差異其實更大。浮點DDA慢就慢在每步都要執行浮點乘加和類型轉換而Cortex-M0沒有硬件浮點單元編譯器會調用軟件浮點庫這一步能把循環拖慢好幾倍。如果屏幕換成分辨率更高的SPI屏I2C瓶頸減小計算占比提升差距會更懸殊。再如果渲染到內存緩沖區后再一次性送屏純計算時間差距能到10倍以上。對于FPGA實現Bresenham的硬件化更是碾壓級的浮點DDA需要乘法器和浮點單元綜合后占用資源大且時鐘頻率低整數Bresenham只需加法器、比較器和幾個寄存器一個狀態機就能在幾個時鐘周期內完成一個像素的計算非常適合硬件光柵器。5.2 整數溢出問題與預防Bresenham的增量值2dy、2(dy-dx)在理論上是安全的但很多人在大屏幕上翻車原因在于用int16_t存坐標。比如dx200、dy100時2dy200沒問題但如果坐標范圍超過32767比如在4096x4096的屏幕上dx可能到40952dy仍在int16范圍內可是中間變量err dx - dy和e2 2 * err可能溢出。具體地說2 * err要存到int16里極限情況err16384時2*err32768直接溢出為負。我見過一個實際案例在高分辨率醫療屏2560x1600的驅動里同事用了short類型結果畫一些斜線時像素出現奇怪的“回跳”排查了半天才發現是溢出導致符號判斷翻轉。預防方法很簡單計算累計值時全部用int32_t最終設置像素時再裁剪到屏幕范圍。除非你確定屏幕分辨率永遠小于8192x8192否則不要理由都不要用16位整數存中間量。5.3 像素坐標邊界與屏幕裁剪策略當線段起點和終點落在屏幕外時Bresenham循環會越界訪問。直接判斷if(x0 xwidth y0 yheight) set_pixel(x,y);可以做但會嚴重影響性能——每一輪循環都多兩次比較。工業級做法是先把線段在CPU側做Cohen-Sutherland或Liang-Barsky裁剪得到完全在屏幕內的新端點再調用Bresenham。裁剪本身只需幾次浮點乘除或整數比較但能讓核心循環減少大量無效迭代。在屏幕很大、但需要繪制的線段較短時這個優化尤其有效。5.4 抗鋸齒Bresenham與現代抗鋸齒的對比很多搞游戲的朋友看到這里可能會問“現在不都有MSAA和FXAA了嗎Bresenham的鋸齒難看啊。”確實如果想要視覺上平滑的線條Bresenham生成的階梯狀邊緣是不夠的。但嵌入式設備為了省內存通常不做全屏抗鋸齒而是用“幾何抗鋸齒”Geometry AA的變體比如Wu算法Xiaolin Wus line algorithm在Bresenham的基礎上同時繪制兩個像素并根據像素中心到理想直線的距離分配亮度。該算法同樣只用整數和少量移位但需要兩個幀緩沖通道或灰度級在帶alpha的LCD上效果很好。形態學后處理Bresenham畫出線后把每個像素的亮度與周圍像素做一次模糊卷積。代價是每像素多幾次訪存但實現簡單。我自己的經驗是如果屏幕本身物理分辨率足夠高超過250PPIBresenham的鋸齒肉眼幾乎不可見不需要抗鋸齒。但在低分屏如128x64 OLED上曲線和斜線的鋸齒非常明顯此時用Wu算法更好。代價是需要灰度控制而單色OLED無法利用灰度只能靠抖動來模擬——這又是另一個話題。6. 常見問題速查與調試技巧這節我把項目中真正頻繁遇到的問題整理成一張速查表方便你直接對照。癥狀可能原因排查思路線條在屏幕上出現“斷點”坐標越界或set_pixel的邊界裁剪寫錯打印x0,y0,x1,y1確認在有效范圍內斜率接近45度時線條明顯“鼓包”誤差項符號判斷反了檢查2*err -dy和2*err dx這兩個條件是否寫成等號關系錯誤從右到左畫線時完全錯亂沒有正確設置sx方向打印每一步的x0確認sx為-1時是否正確遞減大屏幕下線條在中間段突然跳動整數溢出檢查所有中間變量是否int8/int16換成int32負斜率線條邊緣鋸齒特別嚴重斜率絕對值大于1時主軸處理錯誤確認abs(dx)abs(dy)時是否以x為主軸否則以y為主軸嵌入式上畫線明顯慢調用了浮點庫或I2C刷新過慢用內存緩沖先畫到RAM再一次送屏6.1 調試技巧用字符畫驗證算法在沒有圖形環境時怎么快速驗證Bresenham的正確性我的做法是寫一個字符畫版本的測試。用一個二維char數組把所有像素初始化成.把set_pixel改成往數組里寫#最后打印數組。這樣可以在終端直接肉眼檢查線條的連續性和對稱性。void debug_draw_line(int x0, int y0, int x1, int y1) { char canvas[24][80]; memset(canvas, ., sizeof(canvas)); // 替換set_pixel為寫入canvas // 這里省略封裝細節直接跑Bresenham循環 for (int y 0; y 24; y) { for (int x 0; x 80; x) { putchar(canvas[y][x]); } putchar(\n); } }這個方法極其好使特別適合調試八分對稱的圓算法——一個對稱點算錯字符畫里立刻就能看出來。我當年學圖形學時就是靠這個方式把所有Bresenham變體都驗了一遍。6.2 尋找線段上的所有點Bresenham的另一個應用Bresenham的用途不只畫線。在游戲開發中它常用于網格地圖上的“視線檢測”Line of Sight給定一張格子地圖判斷兩個格子之間是否有障礙物遮擋等價于枚舉線段經過的所有格子。這個場景下Bresenham比DDA更優因為它給出的格子序列恰好是“八方向連通”的相鄰格子之間共享頂點或邊不會跳過拐角處的格子。我也在雷達模擬、A*路徑尋優的可視化展示里用過這個思路路徑本來就是格子序列但為了讓線段顯示更自然用Bresenham做插值渲染——每兩個格子之間畫出連續路徑線觀感遠好于直接跳格。6.3 和Bresenham相關的現代替代方案什么時候該換一個常被問到的問題是“都2024年了為什么不用更平滑的曲線或采樣方法”如果是PC端、GPU渲染那你完全不需要手寫Bresenham。GPU的光柵化器內部已經內置了類似Bresenham的硬件單元且支持各種AA、插值、透視矯正。此時手動實現只會更慢且質量更差。但在資源受限的環境里Bresenham依然不可替代MCU/Arduino驅動的OLED/LCD屏FPGA的顯示控制器老式街機模擬器的核心繪制需要精確控制像素輸出的醫療儀表或工業面板在這些領域Bresenham不僅是“夠用”它幾乎就是唯一現實的選擇。因為它只消耗極小的指令周期和寄存器資源不需要FPU、不需要大緩存、不需要動態內存。7. 從畫線算法到坐標體系的完整案例抽象講完了來一個完整的實戰案例在128x64 OLED上畫一個動態旋轉的立方體線框。這個案例能一次性用上Bresenham直線、坐標變換和內存緩沖優化。7.1 場景說明與代碼結構需求以約30FPS的速率旋轉顯示一個立方體的12條邊。傳統做法是每次重繪所有線段。但OLED屏I2C帶寬有限一次全屏刷新需要約4KB數據128x64在I2C 400KHz模式下刷新一次要大約80ms。所以不能每幀都全屏刷新否則幀率只有12FPS而且閃爍嚴重。優化策略使用雙緩沖在RAM里維護一個大小為128*64/8 1024字節的緩沖區所有繪制操作先在這個緩沖區完成最后一次性刷入屏幕。每個像素的繪制變成對緩沖區的位操作速度遠快于I2C寫單個像素。用Bresenham在緩沖區上畫線完全避免浮點和頻繁IO。uint8_t framebuffer[1024]; void set_pixel(int x, int y) { if (x 0 || x 128 || y 0 || y 64) return; framebuffer[y / 8 * 128 x] | 1 (y % 8); } void draw_rotating_cube(float angle) { memset(framebuffer, 0, sizeof(framebuffer)); // 立方體的8個頂點坐標3D float vertices[8][3] { {-1,-1,-1}, {1,-1,-1}, {1,1,-1}, {-1,1,-1}, {-1,-1,1}, {1,-1,1}, {1,1,1}, {-1,1,1} }; // 旋轉矩陣繞Y軸 float cos_a cosf(angle), sin_a sinf(angle); int proj[8][2]; for (int i 0; i 8; i) { float x vertices[i][0] * cos_a vertices[i][2] * sin_a; float z -vertices[i][0] * sin_a vertices[i][2] * cos_a; float y vertices[i][1]; // 簡單正交投影縮放到屏幕坐標 proj[i][0] (int)(x * 20) 64; proj[i][1] (int)(y * 20) 32; } // 12條棱 int edges[12][2] { {0,1},{1,2},{2,3},{3,0}, {4,5},{5,6},{6,7},{7,4}, {0,4},{1,5},{2,6},{3,7} }; for (int e 0; e 12; e) { int p1 edges[e][0], p2 edges[e][1]; draw_line(proj[p1][0], proj[p1][1], proj[p2][0], proj[p2][1]); } // 一次刷屏 ssd1306_buffer_update(framebuffer); }這個工程在這個代碼結構下旋轉立方體的幀率能穩定在25~30FPS完全滿足實時顯示需求。如果用浮點直接每像素寫屏幕幀率可能掉到個位數。7.2 性能優化中的內存布局細節上面代碼里framebuffer[y / 8 * 128 x]這種索引方式在SSD1306上很常見因為它的顯存按頁組織每頁8個像素。但如果你想進一步優化可以把y / 8的除法和1 (y % 8)變成位運算。實際上y / 8 y 3y % 8 y 7這樣能省掉除法器的開銷。在Cortex-M0上無符號除法是調用軟件庫的代價極高——一次除法可能消耗數百個周期。改到位運算后set_pixel的耗時能降低超過一半。這條經驗在幾乎所有嵌入式圖形項目里都適用。7.3 遮擋關系的近似處理線框立方體的用戶體驗取決于線條的正確遮擋。真實3D渲染需要深度緩沖但在MCU上做深度緩沖不現實。簡單的方案是先計算所有棱的中心點到視點的距離按距離排序從遠到近依次繪制。這樣遠處被遮擋的線條會被近處線條覆蓋掉。這個方法對凸多面體的線框特別有效而且只需要排序12個元素開銷極小。我在實際項目里還會再加一個優化判斷立方體旋轉到某個角度時有些面完全不可見就不繪制對應的一組棱。雖然引入了一點分類邏輯但能把每幀的繪制量從12條降到8~9條對提升幀率依然有幫助。8. 我踩過的幾個真實大坑技術文章寫到這基本可以收尾了。但我想補一段純粹的個人經歷——這些坑你不一定會遇到但遇到了能省你一天時間。第一個坑是“精度提升后反而出錯”。有一次我把畫線函數從int16升級到int32理論上應該更安全結果某些線條的像素序列反而變了。原因是對初始決策參數p0的計算方式不同原來int16版本為了防溢出我把p0近似成了2*dy/dx相關的浮點取整而int32版本直接用p0 2*dy - dx。兩者在斜率接近1時的行為不同導致細微差異。最后我統一成標準公式所有版本保持一致才解決。第二個坑是“把1寫成了-1”。在八分圓算法的循環里y的遞減步長寫錯了方向畫出來的圓是“倒葫蘆”形狀排查時還以為是三角函數問題。后來我用字符畫調試法把每一輪的x和y打出來跟手算的幾組數據對比立刻定位到是第三象限映射時的符號搞反了。這個建議所有初學者都訓練一下先把循環的前十步打印出來手算對比前幾步確認無誤再繼續。第三個坑是關于輪廓寬度。Bresenham畫的是1像素寬的線如果需求是3像素寬很多人會畫三條平行線。但這在斜線上會有嚴重的“粗細不均”——因為平行線的間距在斜向投影后變短了。正確做法是以線段為軸畫一個給定寬度的矩形再對矩形做填充。或者用“膨脹法”在像素周圍做3x3的核卷積。這個優化我在PLC工控屏的項目里用到過效果比三條線好太多。9. 后續擴展思考Bresenham這套“誤差累積”的思維方式其實還能延伸到很多其他場景。比如Bresenham風格的多邊形填充掃描線算法里可以借鑒誤差項做邊緣的增量計算。基于Bresenham的紋理映射在低端硬件上近似瓦片紋理的透視矯正效果。網格路徑平滑把格子地圖中將來的走位路徑用Bresenham插值消除生硬的直角轉折。我自己做過一個把Bresenham用于音頻波形繪制的小實驗從音頻采樣點生成波形圖時傳統方式是逐點畫豎線開銷大改成Bresenham后只需要把采樣值映射成y坐標然后在相鄰采樣點之間畫豎線再用Bresenham連接包絡頂點整體渲染效率提升明顯而且波形更平滑。這個方向其實還有很大挖掘空間。現代計算機不缺算力但低功耗設備、IoT面板、還有復古硬件復刻圈子里Bresenham仍然是黃金標準。如果你是在做這類項目好好掌握這個算法會是你工具箱里很趁手的一把螺絲刀。