You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI光线追踪器渲染时UI无法逐像素更新问题排查

问题描述

基于Peter Shirley的教程,我用Swift编写了一款小型光线追踪器,它能正常运行并输出正确的PPM文件。现在我想把它嵌入UI,实现渲染过程中逐像素更新实时刷新画面的效果,因此搭建了一个采用基础MVVM架构的MacOS应用。

目前渲染功能可正常运行,图像能正常显示,但并未实现预期的逐像素更新效果,而是一次性显示完整图像。

Model代码(RGBViewModel.swift)

//
//  RGBViewModel.swift
//  rtweekend_gui
//
//

import SwiftUI

// RGB structure to hold color values
struct RGB {
    var r: UInt8
    var g: UInt8
    var b: UInt8
}

// ViewModel to handle the RGB array and updates
class RGBViewModel: ObservableObject {
    // Define the dimensions of your 2D array
    let width = 1200
    let height = 675
    
    // Published property to trigger UI updates
    @Published var rgbArray: [[RGB]]
    
    init() {
        // Initialize with black pixels
        rgbArray = Array(repeating: Array(repeating: RGB(r: 0, g: 0, b: 0), count: width), count: height)
    }
    
    func render_scene() {
        for j in 0..<height {
            for i in 0..<width {
                
                // Generate a random color
                let r = UInt8.random(in: 0...255)
                let g = UInt8.random(in: 0...255)
                let b = UInt8.random(in: 0...255)
                
                // Update on the main thread since this affects the UI
                DispatchQueue.main.async {
                    // Update the array
                    self.rgbArray[j][i] = RGB(r: r, g: g, b: b)
                }
            }
        }
    }
}

View代码(RGBArrayView.swift)

//
//  RGBArrayView.swift
//  rtweekend_gui
//
//

import SwiftUI

struct RGBArrayView: View {
    // The 2D array of RGB values
    @StateObject private var viewModel = RGBViewModel()
    
    // Control the size of each pixel
    private let pixelSize: CGFloat = 1
    
    var body: some View {
        VStack {
            // Display the RGB array
            Canvas { context, size in
                for y in 0..<viewModel.rgbArray.count {
                    for x in 0..<viewModel.rgbArray[y].count {
                        let rgb = viewModel.rgbArray[y][x]
                        let rect = CGRect(
                            x: CGFloat(x) * pixelSize,
                            y: CGFloat(y) * pixelSize, 
                            width: pixelSize, 
                            height: pixelSize
                        )
                        
                        context.fill(
                            Path(rect),
                            with: .color(Color(
                                red: Double(rgb.r) / 255.0,
                                green: Double(rgb.g) / 255.0,
                                blue: Double(rgb.b) / 255.0
                            ))
                        )
                    }
                }
            }
            .border(Color.gray)
            
            // Button to start filling the array
            Button("Render") {
                viewModel.render_scene()
            }
            .padding()
        }
        .padding()
        .frame(width: CGFloat(viewModel.width) * pixelSize + 40, 
               height: CGFloat(viewModel.height) * pixelSize + 80)
    }
}

// Preview for SwiftUI
struct RGBArrayView_Previews: PreviewProvider {
    static var previews: some View {
        RGBArrayView()
    }
}

解决方案

问题根源

  1. 主线程阻塞与任务批量处理:按钮点击触发render_scene时,该方法运行在主线程。循环中所有DispatchQueue.main.async任务会被一次性添加到主队列,等到整个循环执行完毕后,主队列才会批量处理这些UI更新。SwiftUI会合并这些频繁的@Published属性更新,只执行一次Canvas重绘,因此看起来是一次性显示完整图像。
  2. 缺乏UI刷新间隙:即使渲染任务在后台,若没有给UI线程留出处理更新的时间,SwiftUI仍会合并多次像素更新,无法逐帧显示变化。

修改步骤

1. 将渲染任务移至后台线程,并逐像素触发UI更新

修改RGBViewModel的render_scene方法,使用Task(Swift 5.5+)在后台执行渲染逻辑,每次更新像素后切换到主线程,并给UI留出刷新间隙:

func render_scene() {
    Task {
        for j in 0..<height {
            for i in 0..<width {
                // 生成颜色(实际光线追踪逻辑替换此处)
                let r = UInt8.random(in: 0...255)
                let g = UInt8.random(in: 0...255)
                let b = UInt8.random(in: 0...255)
                
                // 切换到主线程更新UI
                await MainActor.run {
                    self.rgbArray[j][i] = RGB(r: r, g: g, b: b)
                }
                
                // 给UI线程留出刷新时间,可根据需求调整延迟时长
                try? await Task.sleep(nanoseconds: 1)
            }
        }
    }
}

2. 优化Canvas绘制效率(可选)

当前Canvas每次重绘都会遍历所有像素,当分辨率较高时会影响性能。可以改用CGImage直接绘制像素数据,只更新变化区域,但如果是演示级需求,原Canvas逻辑也能工作。

关键说明

  • 使用Task和MainActor.run确保渲染逻辑不阻塞主线程,同时安全更新UI属性。
  • Task.sleep的作用是让UI线程有机会处理当前的更新请求,避免SwiftUI合并多次更新。如果光线追踪本身计算耗时较长,可能不需要额外的sleep,因为计算过程已经给UI留出了间隙。

内容的提问来源于stack exchange,提问作者easythrees

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:35:55