據說你想寫個渲染引擎 - 繪製

你們好,我是微微笑的蝸牛,🐌。css

上一節咱們講了如何肯定節點佈局信息,輸出了佈局樹。今天,將介紹最後一個環節,繪製。內容很少,相對也比較好理解。html

附上前面幾節的連接,想了解的童靴能夠先看看。git

總體流程

整個過程以下:github

  • 根據佈局樹生成繪製命令列表
  • 光柵化,生成像素點信息
  • 將像素轉換爲圖片
  • 顯示到屏幕

繪製命令是個啥?它描述了你該如何繪製一個圖形,好比位置大小、顏色、形狀等等。web

這裏,咱們只處理背景色和邊框的繪製,文字暫不涉及。因此只需支持矩形繪製,知道矩形的位置大小、顏色就好。swift

數據結構

繪製命令,定義爲枚舉類型。暫且只支持矩形,關聯顏色和區域。api

// 繪製命令,目前只支持顏色繪製
enum DisplayCommand {
    case SolidColor(Color, Rect)
}
複製代碼

畫布,用來保存像素信息。數組

// 畫布
struct Canvas {
    var width: Int
    var height: Int
    
    // 像素點,argb
    var pixels: [Color]
}
複製代碼

生成繪製命令

背景

背景的繪製命令比較好生成。從節點樣式表中取出背景色 background,再計算出背景區域就好。markdown

背景區域包括「內容區+內邊距+邊框」。數據結構

image

代碼以下:

// 繪製背景
func renderBackground(list: inout [DisplayCommand], layoutBox: LayoutBox) {
    // 獲取背景色
    if let color = getColor(layoutBox: layoutBox, name: "background") {
        
        // 背景包括 padding + border + content
        let displayCommand = DisplayCommand.SolidColor(color, layoutBox.dimensions.borderBox())
        
        list.append(displayCommand)
    }
}
複製代碼

邊框

一樣,首先從樣式表中取出邊框顏色 border-color

另外,邊框分爲上下左右四個矩形區域,需分別計算出來。以下所示:

image

繪製命令的生成跟背景色差很少,只是注意下矩形區域的計算。

總繪製列表

遞歸遍歷佈局樹,將每一個節點的繪製命令加入到數組,便可獲得總繪製命令列表。

// 生成整體繪製列表
func buildDisplayList(layoutRoot: LayoutBox) -> [DisplayCommand] {
    var list: [DisplayCommand] = []
    
    renderLayoutBox(list: &list, layoutBox: layoutRoot)
    
    return list
}

func renderLayoutBox(list: inout [DisplayCommand], layoutBox: LayoutBox) {
    // 繪製背景
    renderBackground(list: &list, layoutBox: layoutBox)
    
    // 繪製邊框
    renderBorder(list: &list, layoutBox: layoutBox)

    // 遍歷子節點,遞歸生成命令
    for child in layoutBox.children {
        renderLayoutBox(list: &list, layoutBox: child)
    }
}
複製代碼

光柵化

將繪製命令轉換爲像素信息。

繪製命令中包含顏色和區域,將該區域中每一個點的色值寫入像素數組就好。

// 生成像素點
mutating func genPixel(color: Color, rect: Rect) {
    // 將 rect 範圍內的點填充爲 color,不能超過畫布大小
		// clamp 主要是用於限制範圍
    let x0 = Int(rect.x.clamp(min: 0, max: Float(self.width)))
    let y0 = Int(rect.y.clamp(min: 0, max: Float(self.height)))

    let x1 = Int((rect.x + rect.width).clamp(min: 0, max: Float(self.width)))
    let y1 = Int((rect.y + rect.height).clamp(min: 0, max: Float(self.height)))
    
    // 遍歷全部點,橫向一行行填充
    for y in y0...y1  {
        for x in x0...x1 {
            let index = y * width + x
            pixels[index] = color
        }
    }
}
複製代碼

生成圖片

獲得像素信息數組後,使用 CoreGraphics 的 api 生成圖片。注意 color 的順序是 argb

func imageFromARGB32Bitmap(pixels: [Color], width: Int, height: Int) -> UIImage? {
        guard width > 0 && height > 0 else { return nil }
        guard pixels.count == width * height else { return nil }

        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
        let bitsPerComponent = 8
        let bitsPerPixel = 32

        var data = pixels // Copy to mutable []
        guard let providerRef = CGDataProvider(data: NSData(bytes: &data,
                                length: data.count * MemoryLayout<PixelData>.size)
            )
            else { return nil }

        guard let cgim = CGImage(
            width: width,
            height: height,
            bitsPerComponent: bitsPerComponent,
            bitsPerPixel: bitsPerPixel,
            bytesPerRow: width * MemoryLayout<PixelData>.size,
            space: rgbColorSpace,
            bitmapInfo: bitmapInfo,
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
            )
            else { return nil }

        return UIImage(cgImage: cgim)
    }
複製代碼

顯示

這步只需建立一個 ImageView 將 image 顯示出來。

測試數據

測試數據在工程目錄下的 Example 下面,test.html 和 test.css。

效果以下圖所示:

image

完整代碼可點此查看:github.com/silan-liu/t…

總結

這一節主要介紹了繪製的相關操做,重點在於將佈局信息生成繪製列表,而後進行光柵化,轉換爲像素點的過程。

至此,「據說你想寫個渲染引擎」系列已所有完結,感謝您的閱讀~

相關文章
相關標籤/搜索