1. 程式人生 > 其它 >WebGL 與 WebGPU比對[7] - 渲染的目的地

WebGL 與 WebGPU比對[7] - 渲染的目的地

1. 綜述

其實,寫到第六篇比對基本上常規的 API 就差不多比對完了(除了 GPGPU、查詢方面的 API 未涉及),但是有一個細節仍然值得我開一篇比對文章進行思考、記錄,那就是渲染到何處。

WebGL 的上下文物件是與 canvas 元素強關聯的,沒有 canvas 建立不了上下文,也就是說,WebGL 在設計之初就是拿來繪圖的(的確如此),沒考慮 GPU 的其它功能,後來才逐漸加入其它功能。所以說,WebGL 若不顯式指定 Framebuffer,那預設就是畫到 canvas 自己身上。

WebGPU 則更強調“GPU”本身,它是需要自己制定繪製目標的,也就是在通道編碼器中設定的顏色附件關聯的紋理物件。

本篇著重介紹 WebGPU 這一處新設計。有關 FBO 和 RBO 技術與 WebGPU 的差異我另有文章,請自行查閱。

2. WebGL 中的繪圖區

把幀緩衝對映到繪圖視窗,就算完成了。WebGL 需要使用 gl.viewport() 來指定繪圖區的大小:

gl.viewport(0, 0, canvas.width, canvas.height)

通常就是 canvas 的畫素長寬(而不是 CSS 長寬)。

一般頁面變化時要修改:

const resize = (canvas) => {
  // 獲取 css 實際渲染尺寸
  const displayWidth  = canvas.clientWidth;
  const displayHeight = canvas.clientHeight;
 
  // 檢查尺寸是否相同
  if (canvas.width != displayWidth || canvas.height != displayHeight) {
    // 設定為相同的尺寸
    canvas.width  = displayWidth;
    canvas.height = displayHeight;
  }
}

const frame = () => {
  // ...
  resize(gl.canvas)
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
}

你可以獲取當前機器上的最大視口:

gl.getParameter(gl.MAX_VIEWPORT_DIMS)

也可以獲取當前的視口大小:

gl.getParameter(gl.VIEWPORT)

擴充套件知識:ThreeJS 對尺寸變化的處理方式是修改 renderer 的 size,以及修改 camera 的寬高比並更新投影矩陣。

3. WebGPU 中配置 canvas 的連線

在一個常規的 WebGPU 渲染程式中,如果要顯式繪製到 canvas 上,那就要讓 canvas 作為一張紋理,附著到渲染通道的顏色附件上。

// 這一步在請求裝置物件之後就可以進行了
const context = canvasRef.current.getContext('webgpu');
const presentationFormat = context.getPreferredFormat(adapter)

const devicePixelRatio = window.devicePixelRatio || 1
const presentationSize = [
  canvasRef.current.clientWidth * devicePixelRatio,
  canvasRef.current.clientHeight * devicePixelRatio,
]

// 使用裝置物件配置 canvas,讓它變成合適的紋理
context.configure({
  device,
  format: presentationFormat,
  size: presentationSize,
})

然後在渲染通道編碼器就可以用 canvas 這個紋理了:

const frame = () => {
  // ...
  
  // 渲染的每一幀,獲取新的紋理和檢視繫結至渲染通道
  const textureView = context.getCurrentTexture().createView()
  const renderPassDescriptor = {
    colorAttachments: [
      {
        view: textureView,
        loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
        storeOp: 'store',
      },
    ],
  }
  
  // ... draw
  
  requestAnimationFrame(frame)
}

也許你會問,為什麼要這麼複雜?這跟 WebGPU 的使命有關,前面說了,WebGPU 更專注於 GPU 本身,而不是一個簡單的繪圖 API,在 WebGPU 中,渲染繪圖不再是第一優先順序,呼叫 WebGPU 的最大意義就是可以通過統一的 API 訪問 GPU 的計算核心。

每當呼叫 configure() 方法去配置 canvas 紋理時,先前的紋理物件就會被銷燬,並重新生成一個,適合視窗縮放時進行。

當然,你也可以使用 context.unconfigure() 方法僅取消配置,不再生成紋理。

同一個配置前提下,getCurrentTexture() 返回的紋理總是同一個。

如果你補顯式指定配置引數的 size,那麼內部會預設使用 canvas 的繪圖長寬。如果你設定的長寬與 canvas 的繪圖長寬不一致,那麼它會幫你縮放到 canvas 的長寬。

關於改變 canvas 大小後的行為

改變 canvas 的大小,可以是改變其 CSS 渲染大小,也可以改變它的繪製長寬。

規範給了一個簡單的例子,使用 ResizeObserver API 來監聽 canvas 的大小,並重新配置 canvas 紋理:

const canvas = document.createElement('canvas')
const context =  canvas.getContext('webgpu')

const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    // 跳過非 canvas 目標
    if (entry.target != canvas) { 
      continue
    }
    // 為 webgpu 重新配置 canvas 紋理
    context.configure({
      device: gpuDevice,
      format: context.getPreferredFormat(gpuAdapter),
      size: {
        // 獲取到新的長寬
        width: entry.devicePixelContentBoxSize[0].inlineSize,
        height: entry.devicePixelContentBoxSize[0].blockSize,
      }
    })
  }
})

// 僅觀察 canvas
resizeObserver.observe(canvas)