WebGL 最佳實踐

WebGL 是一個複雜的 API,其推薦用法往往不明顯。本頁面針對不同專業水平的使用者提供了建議,不僅強調了應該做和不應該做的事情,還詳細闡述了原因。無論您的使用者執行何種瀏覽器或硬體,您都可以依靠本文件來指導您選擇方法,並確保您走在正確的道路上。

解決並消除 WebGL 錯誤

您的應用程式應在不產生任何 WebGL 錯誤(由 getError 返回)的情況下執行。每個 WebGL 錯誤都會在 Web 控制檯中作為 JavaScript 警告報告,並附有描述性訊息。如果錯誤過多(Firefox 中為 32 個),WebGL 將停止生成描述性訊息,這會嚴重阻礙除錯。

一個格式良好的頁面唯一會生成的錯誤是 OUT_OF_MEMORYCONTEXT_LOST

瞭解擴充套件可用性

大多數 WebGL 擴充套件的可用性取決於客戶端系統。在使用 WebGL 擴充套件時,如果可能,請嘗試透過優雅地適應不支援的情況來使其成為可選。

這些 WebGL 1 擴充套件普遍受支援,可以依賴它們的存在

  • ANGLE_instanced_arrays
  • EXT_blend_minmax
  • OES_element_index_uint
  • OES_standard_derivatives
  • OES_vertex_array_object
  • WEBGL_debug_renderer_info
  • WEBGL_lose_context

(另請參閱:WebGL 特性級別和支援百分比

考慮將這些擴充套件填充到 WebGLRenderingContext 中,例如:https://github.com/kdashg/misc/blob/tip/webgl/webgl-v1.1.js

瞭解系統限制

與擴充套件類似,您的系統限制會與客戶端系統不同!不要僅僅因為您的機器上可以使用每個著色器三十個紋理取樣器就假設您可以這樣做!

WebGL 的最低要求相當低。實際上,幾乎所有系統都至少支援以下內容

MAX_CUBE_MAP_TEXTURE_SIZE: 4096
MAX_RENDERBUFFER_SIZE: 4096
MAX_TEXTURE_SIZE: 4096
MAX_VIEWPORT_DIMS: [4096,4096]
MAX_VERTEX_TEXTURE_IMAGE_UNITS: 4
MAX_TEXTURE_IMAGE_UNITS: 8
MAX_COMBINED_TEXTURE_IMAGE_UNITS: 8
MAX_VERTEX_ATTRIBS: 16
MAX_VARYING_VECTORS: 8
MAX_VERTEX_UNIFORM_VECTORS: 128
MAX_FRAGMENT_UNIFORM_VECTORS: 64
ALIASED_POINT_SIZE_RANGE: [1,100]

您的桌面可能支援 16k 紋理,或者在頂點著色器中支援 16 個紋理單元,但大多數其他系統不支援,因此在您機器上能執行的內容在它們上面可能無法執行!

避免使 FBO 附件繫結失效

幾乎任何對 FBO 附件繫結的更改都會使其幀緩衝區完整性失效。請提前設定好您的熱幀緩衝區。

在 Firefox 中,在 about:config 中將首選項 webgl.perf.max-warnings 設定為 -1 將啟用效能警告,其中包括關於 FBO 完整性失效的警告。

避免更改 VAO 附件(vertexAttribPointer、disable/enableVertexAttribArray)

從靜態、不變的 VAO 繪製比每次繪製呼叫都修改同一個 VAO 要快。對於未更改的 VAO,瀏覽器可以快取獲取限制,而當 VAO 更改時,瀏覽器必須重新驗證和重新計算限制。這樣做的開銷相對較低,但重用 VAO 也意味著更少的 vertexAttribPointer 呼叫,因此只要容易,就值得這樣做。

儘快刪除物件

不要等待垃圾收集器/迴圈收集器意識到物件是孤立的並銷燬它們。實現會跟蹤物件的活躍性,因此在 API 級別“刪除”它們只會釋放引用實際物件的控制代碼。(概念上釋放控制代碼對物件的引用指標)只有當物件在實現中不再使用時,它才會被實際釋放。例如,如果您永遠不想再次直接訪問著色器物件,只需在將它們附加到程式物件後刪除它們的控制代碼即可。

儘快丟失上下文

當您確定不再需要 WebGL 上下文,並且不再需要目標畫布的渲染結果時,也可以考慮透過 WEBGL_lose_context 擴充套件主動丟失 WebGL 上下文。請注意,在離開頁面時無需這樣做——不要僅僅為此目的新增解除安裝事件處理程式。

當期望結果時呼叫 flush

當預期結果(例如查詢)或渲染幀完成時,呼叫 flush()

Flush 告訴實現將所有待處理的命令推出執行,將它們從佇列中重新整理,而不是等待更多命令入隊後再發送執行。

例如,以下情況可能在不丟失上下文的情況下永遠不會完成

js
sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glClientWaitSync(sync, 0, GL_TIMEOUT_IGNORED);

WebGL 預設沒有 SwapBuffers 呼叫,因此 flush 也可以幫助填補空白。

不使用 requestAnimationFrame 時使用 webgl.flush()

當不使用 RAF 時,使用 webgl.flush() 以鼓勵急切執行排隊命令。

由於 RAF 直接跟隨幀邊界,因此使用 RAF 時實際上不需要顯式的 webgl.flush()

在生產環境中避免阻塞 API 呼叫

某些 WebGL 入口點(包括 getErrorgetParameter)會導致呼叫執行緒同步阻塞。即使是基本請求也可能需要長達 1ms 的時間,如果它們需要等待所有圖形工作完成(其效果類似於原生 OpenGL 中的 glFinish()),則可能需要更長時間。

在生產程式碼中,避免使用此類入口點,尤其是在瀏覽器主執行緒上,它們可能導致整個頁面卡頓(通常包括滾動甚至整個瀏覽器)。

  • getError():導致重新整理 + 往返以從 GPU 程序獲取錯誤)。

    例如,在 Firefox 中,僅在分配(bufferData*texImage*texStorage*)之後才檢查 glGetError,以捕獲任何 GL_OUT_OF_MEMORY 錯誤。

  • getShader/ProgramParameter()getShader/ProgramInfoLog()、其他著色器/程式的 get 呼叫:如果未在著色器編譯完成後執行,則會導致重新整理 + 著色器編譯 + 往返。(另請參閱下面的並行著色器編譯。)

  • 一般的 get*Parameter():可能導致重新整理 + 往返。在某些情況下,這些會被快取以避免往返,但儘量避免依賴此行為。

  • checkFramebufferStatus():可能導致重新整理 + 往返。

  • getBufferSubData():通常會導致完成 + 往返。(與圍欄結合使用時,對於 READ 緩衝區來說這沒問題——請參閱下面的非同步資料回讀。)

  • readPixels() 到 CPU(即,未繫結 UNPACK 緩衝區):完成 + 往返。相反,結合非同步資料回讀使用 GPU-GPU readPixels

始終將頂點屬性 0 啟用為陣列

如果您在未將頂點屬性 0 啟用為陣列的情況下進行繪製,則在桌面 OpenGL(例如 macOS)上執行時,您將強制瀏覽器執行復雜的模擬。這是因為在桌面 OpenGL 中,如果頂點屬性 0 未啟用為陣列,則不會繪製任何內容。您可以使用 bindAttribLocation 強制頂點屬性使用位置 0,並使用 enableVertexAttribArray(0) 使其啟用為陣列。

估算每畫素 VRAM 預算

WebGL 不提供查詢系統上最大視訊記憶體量的 API,因為此類查詢不可移植。儘管如此,應用程式仍必須注意 VRAM 使用情況,而不僅僅是儘可能多地分配。

Google 地圖團隊開創的一項技術是每畫素 VRAM 預算的概念

1) 對於一個系統(例如,特定的桌上型電腦/筆記型電腦),決定您的應用程式應該使用的最大 VRAM 量。2) 計算最大化瀏覽器視窗覆蓋的畫素數。例如 (window.innerWidth * devicePixelRatio) * (window.innerHeight * window.devicePixelRatio) 3) 每畫素 VRAM 預算是 (1) 除以 (2),並且是一個常數。

這個常數通常在系統之間是可移植的。移動裝置的螢幕通常比帶有大顯示器的強大桌面機器小。在幾個目標系統上重新計算這個常數以獲得可靠的估計。

現在調整應用程式中的所有內部快取(WebGLBuffers、WebGLTextures 等),使其遵守最大大小,該大小由該常數乘以當前瀏覽器視窗覆蓋的畫素數計算得出。這需要估計每個紋理消耗的位元組數。該上限也必須通常隨著瀏覽器視窗大小的調整而更新,並且超過限制的舊資源必須被清除。

將應用程式的 VRAM 使用量保持在該上限之下將有助於避免記憶體不足錯誤和相關的系統不穩定。

考慮渲染到較小的後臺緩衝區

一種常見的(且簡單)以質量換取速度的方法是渲染到較小的後臺緩衝區,然後將結果放大。考慮減小 canvas.width 和 height,並保持 canvas.style.width 和 height 為固定大小。

批次繪製呼叫

將繪製呼叫“批次處理”成更少、更大的繪製呼叫通常會提高效能。如果您有 1000 個精靈要繪製,請嘗試透過一次 drawArrays() 或 drawElements() 呼叫來完成。

如果您需要將不連續的物件繪製為單個 drawArrays(TRIANGLE_STRIP) 呼叫,通常會使用“退化三角形”。退化三角形是沒有面積的三角形,因此任何一個點或多個點位於完全相同位置的三角形都是退化三角形。這些三角形實際上會被跳過,這讓您可以開始一個新的三角形帶,而不必將其與前一個三角形帶連線,也無需拆分為多個繪製呼叫。

另一種重要的批處理方法是紋理圖集,其中多個影像被放置到一個紋理中,通常像棋盤一樣。由於您需要分割繪製呼叫批次以更改紋理,因此紋理圖集允許您將更多繪製呼叫組合成更少、更大的批次。請參閱此示例,演示如何將甚至引用多個紋理圖集的精靈組合成單個繪製呼叫。

避免 "#ifdef GL_ES"

您絕不應該在 WebGL 著色器中使用 #ifdef GL_ES;在 WebGL 中,此條件始終為真。儘管一些早期示例使用了它,但它並非必需。

優先在頂點著色器中完成工作

在頂點著色器中完成儘可能多的工作,而不是在片段著色器中。這是因為對於每次繪製呼叫,片段著色器通常比頂點著色器執行更多次。任何可以在頂點上完成的計算,然後透過 varyings 在片段之間插值,都會帶來效能提升。(varying 的插值非常廉價,並透過圖形管道的固定功能光柵化階段自動為您完成。)

例如,可以透過紋理座標隨時間變化的變換來實現紋理表面的簡單動畫。(最簡單的情況是向紋理座標屬性向量新增一個均勻向量)如果視覺上可接受,可以在頂點著色器而不是片段著色器中變換紋理座標,以獲得更好的效能。

一個常見的折衷方案是按頂點而不是按片段(畫素)進行一些光照計算。在某些情況下,特別是對於簡單模型或密集頂點,這看起來已經足夠好。

相反的情況是,如果一個模型的頂點數量多於渲染輸出中的畫素數量。然而,LOD 網格通常是解決此問題的方法,很少將工作從頂點轉移到片段著色器。

按順序編譯著色器和連結程式很有誘惑力,但許多瀏覽器可以在後臺執行緒上並行編譯和連結。

而不是

js
function compileOnce(gl, shader) {
  if (shader.compiled) return;
  gl.compileShader(shader);
  shader.compiled = true;
}
for (const [vs, fs, prog] of programs) {
  compileOnce(gl, vs);
  compileOnce(gl, fs);
  gl.linkProgram(prog);
  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
    console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
    console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
  }
}

考慮

js
function compileOnce(gl, shader) {
  if (shader.compiled) return;
  gl.compileShader(shader);
  shader.compiled = true;
}
for (const [vs, fs, prog] of programs) {
  compileOnce(gl, vs);
  compileOnce(gl, fs);
}
for (const [vs, fs, prog] of programs) {
  gl.linkProgram(prog);
}
for (const [vs, fs, prog] of programs) {
  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
    console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
    console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
  }
}

優先使用 KHR_parallel_shader_compile

雖然我們已經描述了一種允許瀏覽器並行編譯和連結的模式,但通常檢查 COMPILE_STATUSLINK_STATUS 會阻塞直到編譯或連結完成。在可用此擴充套件的瀏覽器中,KHR_parallel_shader_compile 擴充套件提供了一個非阻塞COMPLETION_STATUS 查詢。優先啟用和使用此擴充套件。

使用示例

js
ext = gl.getExtension("KHR_parallel_shader_compile");
gl.compileProgram(vs);
gl.compileProgram(fs);
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);

// Store program in your data structure.
// Later, for example the next frame:

if (ext) {
  if (gl.getProgramParameter(prog, ext.COMPLETION_STATUS_KHR)) {
    // Check program link status; if OK, use and draw with it.
  }
} else {
  // Program linking is synchronous.
  // Check program link status; if OK, use and draw with it.
}

此技術可能不適用於所有應用程式,例如那些需要程式立即可用進行渲染的應用程式。儘管如此,請考慮不同變體可能如何工作。

除非連結失敗,否則不要檢查著色器編譯狀態

導致著色器編譯失敗,但無法推遲到連結時處理的錯誤很少。ESSL3 規範在“錯誤處理”部分對此進行了說明

實現應儘可能早地報告錯誤,但在任何情況下都必須滿足以下條件

  • 在呼叫 glLinkProgram 後,必須檢測到所有詞法、語法和語義錯誤
  • 在呼叫 glLinkProgram 後,必須檢測到由於頂點著色器和片段著色器不匹配(連結錯誤)而導致的錯誤
  • 在任何繪製呼叫或呼叫 glValidateProgram 後,必須檢測到由於超出資源限制而導致的錯誤
  • 呼叫 glValidateProgram 必須報告給定當前 GL 狀態下與程式物件相關的所有錯誤。

編譯器和連結器之間的任務分配是實現相關的。因此,許多錯誤可能在編譯時或連結時檢測到,具體取決於實現。

此外,查詢編譯狀態是同步呼叫,這會中斷管道。

而不是

js
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
  console.error(`vs compile failed: ${gl.getShaderInfoLog(vs)}`);
}

gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
  console.error(`fs compile failed: ${gl.getShaderInfoLog(fs)}`);
}

gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
  console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
}

考慮

js
gl.compileShader(vs);
gl.compileShader(fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
  console.error(`Link failed: ${gl.getProgramInfoLog(prog)}`);
  console.error(`vs info-log: ${gl.getShaderInfoLog(vs)}`);
  console.error(`fs info-log: ${gl.getShaderInfoLog(fs)}`);
}

GLSL 精度註解要精確

如果您期望在著色器之間傳遞一個 essl300 int,並且您需要它是 32 位的,那麼您必須使用 highp,否則您將遇到可移植性問題。(在桌面端有效,但在 Android 上無效)

如果您有浮點紋理,iOS 要求您使用 highp sampler2D foo;,否則它會非常痛苦地給您 lowp 紋理取樣!(+/-2.0 最大值可能對您來說不夠好)

隱式預設值

頂點語言具有以下預宣告的全域性作用域預設精度語句

glsl
precision highp float;
precision highp int;
precision lowp sampler2D;
precision lowp samplerCube;

片段語言具有以下預宣告的全域性作用域預設精度語句

glsl
precision mediump int;
precision lowp sampler2D;
precision lowp samplerCube;

在 WebGL 1 中,片段著色器中的“highp float”支援是可選的

在片段著色器中無條件使用 highp 精度將導致您的內容在某些較舊的移動硬體上無法正常工作。

雖然您可以使用 mediump float 代替,但請注意,這通常會導致由於精度不足而損壞渲染(尤其是在移動系統上),儘管在典型的臺式計算機上這種損壞可能不可見。

如果您瞭解您的精度要求,getShaderPrecisionFormat() 會告訴您系統支援什麼。

如果 highp float 可用,則 GL_FRAGMENT_PRECISION_HIGH 將定義為 1

“始終給我最高精度”的一個好模式

glsl
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif

ESSL100 最低要求 (WebGL 1)

float 思考 range 最小非零 精度
highp float24* (-2^62, 2^62) 2^-62 2^-16 相對
mediump IEEE float16 (-2^14, 2^14) 2^-14 2^-10 相對
lowp 10 位有符號定點 (-2, 2) 2^-8 2^-8 絕對
int 思考 range
highp int17 (-2^16, 2^16)
mediump int11 (-2^10, 2^10)
lowp int9 (-2^8, 2^8)

*float24:符號位,7 位指數,16 位尾數。

ESSL300 最低要求 (WebGL 2)

float 思考 range 最小非零 精度
highp IEEE float32 (-2^126, 2^127) 2^-126 2^-24 相對
mediump IEEE float16 (-2^14, 2^14) 2^-14 2^-10 相對
lowp 10 位有符號定點 (-2, 2) 2^-8 2^-8 絕對
(u)int 思考 int 範圍 unsigned int 範圍
highp (u)int32 [-2^31, 2^31] [0, 2^32]
mediump (u)int16 [-2^15, 2^15] [0, 2^16]
lowp (u)int9 [-2^8, 2^8] [0, 2^9]

優先使用內建函式而不是自己實現

優先使用 dotmixnormalize 等內建函式。在最好的情況下,自定義實現可能與它們所替代的內建函式一樣快,但不要期望它們都能做到。硬體通常對內建函式有超最佳化甚至專門的指令,編譯器無法可靠地用特殊的內建程式碼路徑替換您自定義的內建函式替代品。

對於所有將在 3D 中看到的紋理,請使用 Mipmap

如有疑問,在紋理上傳後呼叫 generateMipmaps()。Mipmap 對記憶體的開銷很小(僅 30%),但在 3D 場景中紋理“縮放縮小”或整體縮小距離時,甚至對於立方體貼圖,它們都能帶來顯著的效能優勢!

由於更好的固有紋理獲取快取區域性性,從較小的紋理影像取樣更快:對非 mipmap 紋理進行縮放縮小會破壞紋理獲取快取區域性性,因為相鄰畫素不再從相鄰紋素取樣!

但是,對於從不“縮放縮小”的 2D 資源,不要為 mipmap 支付 30% 的記憶體附加費

js
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // Defaults to NEAREST_MIPMAP_LINEAR, for mipmapping!

(在 WebGL 2 中,您應該只使用 texStorage 並設定 levels=1

一個注意事項:generateMipmaps 僅在您可以將紋理渲染到幀緩衝區時才有效。(規範稱之為“顏色可渲染格式”)例如,如果系統支援浮點紋理但不支援渲染到浮點紋理,則 generateMipmaps 將對浮點格式失敗。

不要假設可以渲染到浮點紋理

有許多許多系統支援 RGBA32F 紋理,但如果您將其附加到幀緩衝區,您將從 checkFramebufferStatus() 獲得 FRAMEBUFFER_INCOMPLETE_ATTACHMENT。它可能在您的系統上工作,但大多數移動系統不支援它!

在 WebGL 1 上,使用 EXT_color_buffer_half_floatWEBGL_color_buffer_float 擴充套件分別檢查對 float16 和 float32 的渲染到浮點紋理支援。

在 WebGL 2 上,EXT_color_buffer_float 檢查對 float32 和 float16 的渲染到浮點紋理支援。EXT_color_buffer_half_float 存在於僅支援渲染到 float16 紋理的系統上。

渲染到 float32 並不意味著 float32 混合!

它可能在您的系統上工作,但在許多其他系統上不行。如果可以,請避免使用它。檢查 EXT_float_blend 擴充套件以檢查支援情況。

Float16 混合始終受支援。

某些格式(例如,RGB)可能會被模擬

許多格式(特別是三通道格式)是模擬的。例如,RGB32F 通常實際上是 RGBA32F,而 Luminance8 實際上可能是 RGBA8。特別是 RGB8 通常出奇地慢,因為遮蔽 alpha 通道和/或修補混合函式具有相當高的開銷。為了獲得更好的效能,優先使用 RGBA8 並自行忽略 alpha。

避免使用 alpha:false,這可能會很昂貴

在上下文建立期間指定 alpha:false 會導致瀏覽器將 WebGL 渲染的畫布合成成不透明的,忽略應用程式在其片段著色器中寫入的任何 alpha 值。不幸的是,在某些平臺上,此功能會帶來顯著的效能成本。RGB 後臺緩衝區可能必須在 RGBA 表面之上進行模擬,並且 OpenGL API 中可用的技術相對較少,無法使應用程式看起來 RGBA 表面沒有 alpha 通道。已發現所有這些技術對受影響平臺的影響效能大致相同。

大多數應用程式,即使是那些需要 alpha 混合的應用程式,也可以構造為生成 1.0 作為 alpha 通道。主要的例外是任何在混合函式中需要目標 alpha 的應用程式。如果可行,建議這樣做而不是使用 alpha:false

考慮壓縮紋理格式

雖然 JPG 和 PNG 在網路傳輸中通常更小,但 GPU 壓縮紋理格式在 GPU 記憶體中更小,並且取樣速度更快。(這減少了紋理記憶體頻寬,這在移動裝置上非常寶貴)然而,壓縮紋理格式的質量不如 JPG,並且通常只適用於顏色(例如,不適用於法線或座標)。

不幸的是,沒有一個普遍支援的單一格式。但是每個系統都至少有一個以下格式

  • WEBGL_compressed_texture_s3tc (桌面)
  • WEBGL_compressed_texture_etc1 (Android)
  • WEBGL_compressed_texture_pvrtc (iOS)

WebGL 2 透過結合以下方式實現普遍支援

  • WEBGL_compressed_texture_s3tc (桌面)
  • WEBGL_compressed_texture_etc (移動)

WEBGL_compressed_texture_astc 具有更高的質量和/或更高的壓縮率,但僅在較新的硬體上受支援。

Basis Universal 紋理壓縮格式/庫

Basis Universal 解決了上面提到的幾個問題。它提供了一種透過單個壓縮紋理檔案支援所有常見壓縮紋理格式的方法,透過一個在載入時高效轉換格式的 JavaScript 庫。它還添加了額外的壓縮,使 Basis Universal 壓縮紋理檔案在網路傳輸中比普通壓縮紋理小得多,更接近 JPEG。

https://github.com/BinomialLLC/basis_universal/blob/master/webgl/README.md

深度和模板格式的記憶體使用情況

在許多裝置上,深度和模板附件及其格式實際上是不可分離的。您可能會要求 DEPTH_COMPONENT24 或 STENCIL_INDEX8,但幕後通常會得到 D24X8 和 X24S8 32bpp 格式。假設深度和模板格式的記憶體使用量向上取整到最接近的四個位元組。

texImage/texSubImage 上傳(尤其是影片)可能導致管道重新整理

大多數從 DOM 元素上傳的紋理都會導致一個處理過程,該過程會在內部臨時切換 GL 程式,從而導致管道重新整理。(管道在 Vulkan 等中明確形式化,但在 OpenGL 和 WebGL 中隱式地在幕後。管道或多或少是著色器程式、深度/模板/多重取樣/混合/光柵化狀態的元組)

在 WebGL 中

glsl
    …
    useProgram(prog1)
<pipeline flush>
    bindFramebuffer(target)
    drawArrays()
    bindTexture(webgl_texture)
    texImage2D(HTMLVideoElement)
    drawArrays()
    …

瀏覽器內部

glsl
    …
    useProgram(prog1)
<pipeline flush>
    bindFramebuffer(target)
    drawArrays()
    bindTexture(webgl_texture)
    -texImage2D(HTMLVideoElement):
        +useProgram(_internal_tex_transform_prog)
<pipeline flush>
        +bindFramebuffer(webgl_texture._internal_framebuffer)
        +bindTexture(HTMLVideoElement._internal_video_tex)
        +drawArrays() // y-flip/colorspace-transform/alpha-(un)premultiply
        +bindTexture(webgl_texture)
        +bindFramebuffer(target)
        +useProgram(prog1)
<pipeline flush>
    drawArrays()
    …

優先在開始繪製之前或至少在管道之間進行上傳

在 WebGL 中

glsl
    …
    bindTexture(webgl_texture)
    texImage2D(HTMLVideoElement)
    useProgram(prog1)
<pipeline flush>
    bindFramebuffer(target)
    drawArrays()
    bindTexture(webgl_texture)
    drawArrays()
    …

瀏覽器內部

glsl
    …
    bindTexture(webgl_texture)
    -texImage2D(HTMLVideoElement):
        +useProgram(_internal_tex_transform_prog)
<pipeline flush>
        +bindFramebuffer(webgl_texture._internal_framebuffer)
        +bindTexture(HTMLVideoElement._internal_video_tex)
        +drawArrays() // y-flip/colorspace-transform/alpha-(un)premultiply
        +bindTexture(webgl_texture)
        +bindFramebuffer(target)
    useProgram(prog1)
<pipeline flush>
    bindFramebuffer(target)
    drawArrays()
    bindTexture(webgl_texture)
    drawArrays()
    …

使用 texStorage 建立紋理

WebGL 2.0 texImage* API 允許您獨立定義每個 mip 級別並以任何大小,即使是錯誤的 mip 大小也不是錯誤,直到繪製時才出現錯誤,這意味著驅動程式實際上無法在首次繪製紋理之前在 GPU 記憶體中準備紋理。

此外,一些驅動程式可能會無條件地分配整個 mip 鏈(+30% 記憶體!),即使您只想要一個級別。

因此,在 WebGL 2 中,對於紋理,優先使用 texStorage + texSubImage

使用 invalidateFramebuffer

儲存您不再使用的資料可能會導致高昂的成本,尤其是在移動裝置上常見的瓦片式渲染 GPU 上。當您處理完幀緩衝區附件的內容後,使用 WebGL 2.0 的 invalidateFramebuffer 來丟棄資料,而不是讓驅動程式浪費時間儲存資料以供以後使用。特別是 DEPTH/STENCIL 和/或多重取樣附件是 invalidateFramebuffer 的絕佳選擇。

使用非阻塞非同步資料回讀

readPixelsgetBufferSubData 這樣的操作通常是同步的,但是使用相同的 API,可以實現非阻塞、非同步資料回讀。WebGL 2 中的方法類似於 OpenGL 中的方法:阻塞 API 中的非同步下載

js
function clientWaitAsync(gl, sync, flags, interval_ms) {
  return new Promise((resolve, reject) => {
    function test() {
      const res = gl.clientWaitSync(sync, flags, 0);
      if (res === gl.WAIT_FAILED) {
        reject(new Error("clientWaitSync failed"));
        return;
      }
      if (res === gl.TIMEOUT_EXPIRED) {
        setTimeout(test, interval_ms);
        return;
      }
      resolve();
    }
    test();
  });
}

async function getBufferSubDataAsync(
  gl,
  target,
  buffer,
  srcByteOffset,
  dstBuffer,
  /* optional */ dstOffset,
  /* optional */ length,
) {
  const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
  gl.flush();

  await clientWaitAsync(gl, sync, 0, 10);
  gl.deleteSync(sync);

  gl.bindBuffer(target, buffer);
  gl.getBufferSubData(target, srcByteOffset, dstBuffer, dstOffset, length);
  gl.bindBuffer(target, null);

  return dstBuffer;
}

async function readPixelsAsync(gl, x, y, w, h, format, type, dest) {
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf);
  gl.bufferData(gl.PIXEL_PACK_BUFFER, dest.byteLength, gl.STREAM_READ);
  gl.readPixels(x, y, w, h, format, type, 0);
  gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);

  await getBufferSubDataAsync(gl, gl.PIXEL_PACK_BUFFER, buf, 0, dest);

  gl.deleteBuffer(buf);
  return dest;
}

devicePixelRatio 和高 DPI 渲染

處理 devicePixelRatio !== 1.0 很棘手。雖然常見的方法是設定 canvas.width = width * devicePixelRatio,但這會由於 devicePixelRatio 的非整數值(這在 Windows 上的 UI 縮放以及所有平臺上的縮放中很常見)而導致摩爾紋偽影。

相反,我們可以對 CSS 的 top/bottom/left/right 使用非整數值,以相當可靠地將畫布“預對齊”到整數裝置座標。

演示:裝置畫素預對齊

ResizeObserver 和 'device-pixel-content-box'

支援的瀏覽器上,ResizeObserver 可以與 'device-pixel-content-box' 一起使用,以請求一個包含元素真實裝置畫素大小的回撥。這可以用於構建一個非同步但精確的函式

js
function getDevicePixelSize(elem) {
  return new Promise((resolve) => {
    const observer = new ResizeObserver(([cur]) => {
      if (!cur) {
        throw new Error(
          `device-pixel-content-box not observed for elem ${elem}`,
        );
      }
      const devSize = cur.devicePixelContentBoxSize;
      const ret = {
        width: devSize[0].inlineSize,
        height: devSize[0].blockSize,
      };
      resolve(ret);
      observer.disconnect();
    });
    observer.observe(elem, { box: "device-pixel-content-box" });
  });
}

在可用時使用 WEBGL_provoking_vertex

在將頂點組裝成三角形和線等圖元時,按照 OpenGL 的約定,圖元的最後一個頂點被認為是“引發頂點”。這在使用 ESSL300 (WebGL 2) 中的 flat 頂點屬性插值時相關;引發頂點的屬性值用於圖元的所有頂點。

如今,許多瀏覽器的 WebGL 實現都託管在不同於 OpenGL 的圖形 API 之上,其中一些 API 將第一個頂點用作繪製命令的引發頂點。在其中一些 API 上模擬 OpenGL 的引發頂點約定可能會產生計算開銷。

因此,引入了 WEBGL_provoking_vertex 擴充套件。如果 WebGL 實現公開此擴充套件,則這是嚮應用程式發出的提示,表明將約定更改為 FIRST_VERTEX_CONVENTION_WEBGL 將提高效能。強烈建議使用平面著色(flat shading)的應用程式檢查此擴充套件是否存在,並在可用時使用它。請注意,這可能需要更改應用程式的頂點緩衝區或著色器。