OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式
OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式
--是什么(答案是具體值) VS 是不是(答案是布爾值)
目錄
背景介紹
問題描述
Codea 是 iPad 上的一款很方便的開發(fā)軟件, 尤其是它支持 OpenGL ES 2.0/3.0, 支持著色器 shader, 可以直接寫代碼操縱 GPU. 不過也有不太方便的地方, 那就是在 Codea 上寫 OpenGL ES 2.0 Shader 代碼的時候發(fā)現跟蹤 shader 內部使用的變量特別困難, 因為 GPU 就像一個黑洞, 程序員可以通過程序向 vertex shader 和 fragment shader 傳遞數據進去, 但是卻沒辦法把 shader 的變量值傳回來, 這樣就導致在調試 shader 代碼時看不到內部的變化, 有時候出了問題就得左右推測, 以往 打印/輸出 變量值的調試方法也失效了, 結果使得調試 shader 代碼比較困難.
已知條件
但是 shader 還是要輸出信息的, 只不過它輸出的信息是 gl_Position 和 gl_FragColor, 前者是一個四維向量用于設定屏幕圖像像素點坐標, 后者也是一個四維向量用于設定顏色值, 而這兩個信息是無法直接為我們輸出變量值的. 那么是否可以做一點文章, 通過一種間接的方式來傳遞我們需要知道的信息呢?
解決思路
轉換思維
昨天晚上, 在調試一段簡單但是比較有趣的 shader 代碼時, 忽然產生了一個靈感:為什么不改變一下對 shader 提問的方式? 我們之前在調試普通程序時使用的 打印/輸出 技巧實際上等價于向計算機提出一個問題: 請告訴我那個變量的值是多少? 很顯然, shader 程序沒有辦法直接告訴我們那個變量是多少, 那么換一個思維, 改成問計算機: 請告訴我那個變量的值是不是大于100? 這下就簡單了, 因為 shader 是很容易回答這個問題的, 因為這個問題的答案要么是 true, 要么是 false.
第一種簡單方案
我們很容易設計一段 shader 繪圖代碼, 如果答案是 true, 那么我們在指定區(qū)域揮著紅色, 如果答案是 false, 那么我們在指定區(qū)域繪制綠色, 這樣 GPU 就可以通過屏幕間接地把我們需要的信息傳遞出來了.
假設要觀察的變量為 myVar, 想要觀察它是否大于100, shader實現代碼如下:
//這段代碼放在 fragment shader 的 main 函數的最后面
void main()
...
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 設定調試區(qū)顯示范圍為右上角
if(x > 0.9 && y > 0.9) {
if(myVar > 100){
// 答案為 true 則設置調試區(qū)顏色為紅色
gl_FragColor = vec4(1,0,0,.5);
}else{
// 答案為 false 則設置調試區(qū)顏色為紅色
gl_FragColor = vec4(1,0,0,.5);
}
}
}
完整的 shader 代碼為:
myShader = {
vsBase = [[
// vertex shader 代碼
uniform mat4 modelViewProjection;
uniform vec2 uResolution;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main() {
vColor=color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
}
]],
fsBase = [[
// fragment shader 代碼
precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main() {
lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
// 默認全部設置為白色
gl_FragColor = vec4(1,1,1,1);
// 測試變量 myVar, 可分別設置為 >100 和 <=100 兩個區(qū)間的值
int myVar = 1;
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 設定調試區(qū)顯示范圍為右上角
if(x > 0.9 && x < 1.0 && y > 0.9 && y < 1.0) {
if(myVar > 100){
// 答案為 true 則設置調試區(qū)顏色為紅色
gl_FragColor = vec4(1,0,0,1);
}else {
// 答案為 false 則設置調試區(qū)顏色為紅色
gl_FragColor = vec4(0,1,0,1);
}
}
}
]]
}
配套的 Codea 代碼為:
-- Shader debug
displayMode(OVERLAY)
function setup()
m = mesh()
-- 首先得有頂點,后續(xù)要把頂點數據傳給 vertex Shader 處理程序
m:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
-- 設置 shader
m.shader = shader(myShader.vsBase,myShader.fsBase)
-- 要有 texture 才能設置顏色
-- m.texture = "Documents:univer"
m:setColors(color(220,200,200,255))
-- 觀察
parameter.watch("m.shader.modelViewProjection")
parameter.watch("m.shader.uResolution")
parameter.watch("m.vertices[1]")
end
function draw()
background(0)
m:draw()
end
function touched(touch)
end
沒有Codea的用戶可以在XCode下編譯該項目, 然后在模擬器查看執(zhí)行結果, XCode 項目文件下載地址XCode項目文件鏈接:
運行截圖如下:
myVar 大于 100

myVar 小于等于 100

可以顯示變量值的方案
非常好, 通過上面的試驗, 我們終于可以大致了解 shader 中變量的情況了, 比如說它是不是大于某個數, 是不是小于某個數, 是不是正數, 是不是負數, 等等. 但是這種調試信息還是太粗糙, 而且用起來也比較繁瑣. 那么更進一步, 我們還是希望能看到變量的具體的值, 前面說過 shader 沒辦法像 printf 一樣, 直接把變量值打印到屏幕, 但是我們知道我們實際上可以通過 shader 完全控制屏幕輸出, 所以理論上我們可以在屏幕上繪制出任何內容, 包括數字.
現在先簡化一下問題, 假設 myVar 的取值范圍是整數 0~9, 那么我們可以設計一種對應算法, 處理邏輯是這樣的:
如果 myVar 是 1, 那么我們在指定的區(qū)域內把指定的像素點用指定的顏色繪制(繪制出1);
如果 myVar 是 2, 那么我們在指定的區(qū)域內把指定的像素點用指定的顏色繪制(繪制出2);
...
如果 myVar 是 9, 那么我們在指定的區(qū)域內把指定的像素點用指定的顏色繪制(繪制出9);
如果 myVar 是 0, 那么我們在指定的區(qū)域內把指定的像素點用指定的顏色繪制(繪制出0);
聽起來不錯, 這樣我們就可以讓 shader 輸出 1~0 10個數字了, 繼續(xù)簡化問題, 先從最簡單的地方入手, 我們試著處理一下 1, 暫時不管 myVar 的值, 我們只是簡單地在屏幕上繪制一個 1, 那么代碼如下:
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 先設置好調試區(qū)的范圍
if(x > 0.9 && x < 1.0 && y > 0.9 && y < 1.0) {
// 設置正方形區(qū)域的顏色為白色作為背景色
gl_FragColor = vec4(1,1,1,1);
// 相當于在一個正方形內繪制一個 `1`, 我們選擇最右邊
if( x > 0.99 ){
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
截圖如下:
很好, 繼續(xù), 增加一個判斷條件 myVar是否為1, 否則只要執(zhí)行到這個區(qū)域坐標一律會繪制一個白底綠色的數字1, 如下:
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 先設置好調試區(qū)的范圍
if(x > 0.9 && x < 1.0 && y > 0.9 && y < 1.0) {
// 設置正方形區(qū)域的顏色為白色作為背景色
gl_FragColor = vec4(1,1,1,1);
// 相當于在一個正方形內繪制一個 `1`, 我們選擇最右邊
if( myVar == 1 && x > 0.99 ){
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
接著我們可以把 2~0 的數字全部以這種方式繪制出來, 為了簡單起見, 數字造型全部采用類似7段數碼管的那種風格, 偽碼如下:
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 先設置好調試區(qū)的范圍
if(x > 0.9 && x < 1.0 && y > 0.9 && y < 1.0) {
// 設置正方形區(qū)域的顏色為白色作為背景色
gl_FragColor = vec4(1,1,1,1);
// 相當于在一個正方形內繪制一個 `1`, 我們選擇最右邊
if( myVar == 1 && x > 0.99 ){
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
if( myVar == 2 && (2的繪制坐標范圍) ){
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
...
if( myVar == 0 && (0的坐標繪制范圍) ){
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
回頭看看代碼, 發(fā)現貌似有很多重復的地方, 稍微合并一下, 偽碼如下:
// 取得坐標
float x = vTexCoord.x;
float y = vTexCoord.y;
// 先設置好調試區(qū)的范圍
if(x > 0.9 && x < 1.0 && y > 0.9 && y < 1.0) {
// 設置正方形區(qū)域的顏色為白色作為背景色
gl_FragColor = vec4(1,1,1,1);
// 相當于在一個正方形內繪制一個 `1`, 我們選擇最右邊
if(( myVar == 1 && x > 0.99 ) ||
( myVar == 2 && (2的繪制坐標范圍)) ||
...
( myVar == 0 && (0的坐標繪制范圍))
)
{
// 最右邊設置為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
基本上就是這樣樣子, 把它寫成函數形式, 代碼如下:
// 構造數字
void ledChar(int n, float xa,float xb, float ya, float yb){
float x = vTexCoord.x;
float y = vTexCoord.y;
float x1 = xa;
float x2 = xa+xb;
float y1 = ya;
float y2 = ya+yb;
float ox = (x2+x1)/2.0;
float oy = (y2+y1)/2.0;
float dx = (x2-x1)/10.0;
float dy = (y2-y1)/10.0;
float b = (x2-x1)/20.0;
int num = n;
// 設定調試區(qū)顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區(qū)背景色的半透明的藍色
gl_FragColor = vec4(0,0,1,.5);
// 分別繪制出 LED 形式的數字 1~0
if((num==1 && (x > x2-dx)) ||
(num==2 && ((y > y2-dy) || (x > x2-dx && y > oy-dy/2.0) || (y > oy-dy/2.0 && y < oy+dy/2.0) || (x < x1+dx && y < oy+dy/2.0) || (y < y1+dy))) ||
(num==3 && ((y > y2-dy) || (x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0) || (y < y1+dy))) ||
(num==4 && ((x < x1+dx && y > oy-dy/2.0) ||(x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0))) ||
(num==5 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
(num==6 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
(num==7 && ((y > y2-dy) || (x > x2-dx))) ||
(num==8 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx) || (y<y1+dy))) ||
(num==9 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)||(y > oy-dy/2.0 && y < oy+dy/2.0)|| (x>x2-dx) || (y<y1+dy))) ||
(num==0 && ((y > y2-dy) || (x < x1+dx) || (x>x2-dx) || (y<y1+dy)))
)
{
// 設置數字顏色為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
}
完整的 shader 代碼如下:
myShader = {
vsBase = [[
// vertex shader 代碼
uniform mat4 modelViewProjection;
uniform vec2 uResolution;
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void main() {
vColor=color;
vTexCoord = texCoord;
gl_Position = modelViewProjection * position;
}
]],
fsBase = [[
// fragment shader 代碼
precision highp float;
uniform lowp sampler2D texture;
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
void ledChar(int,float,float,float,float);
void main() {
lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
// 默認全部設置為黑色
gl_FragColor = vec4(.1,.1,.1,1);
// 在右上角顯示1
ledChar(1, 0.9, 0.1, 0.9, 0.1);
}
// 構造數字
void ledChar(int n, float xa,float xb, float ya, float yb){
float x = vTexCoord.x;
float y = vTexCoord.y;
float x1 = xa;
float x2 = xa+xb;
float y1 = ya;
float y2 = ya+yb;
float ox = (x2+x1)/2.0;
float oy = (y2+y1)/2.0;
float dx = (x2-x1)/10.0;
float dy = (y2-y1)/10.0;
float b = (x2-x1)/20.0;
int num = n;
// 設定調試區(qū)顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區(qū)背景色為半透明的藍色
gl_FragColor = vec4(0.2,0.8,0.2,.5);
// 分別繪制出 LED 形式的數字 1~0
if((num==1 && (x > x2-dx)) ||
(num==2 && ((y > y2-dy) || (x > x2-dx && y > oy-dy/2.0) || (y > oy-dy/2.0 && y < oy+dy/2.0) || (x < x1+dx && y < oy+dy/2.0) || (y < y1+dy))) ||
(num==3 && ((y > y2-dy) || (x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0) || (y < y1+dy))) ||
(num==4 && ((x < x1+dx && y > oy-dy/2.0) ||(x > x2-dx) || (y > oy-dy/2.0 && y < oy+dy/2.0))) ||
(num==5 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
(num==6 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx && y <oy-dy/2.0) || (y<y1+dy))) ||
(num==7 && ((y > y2-dy) || (x > x2-dx))) ||
(num==8 && ((y > y2-dy) || (x < x1+dx)|| (y > oy-dy/2.0 && y < oy+dy/2.0) || (x>x2-dx) || (y<y1+dy))) ||
(num==9 && ((y > y2-dy) || (x < x1+dx && y > oy-dy/2.0)||(y > oy-dy/2.0 && y < oy+dy/2.0)|| (x>x2-dx) || (y<y1+dy))) ||
(num==0 && ((y > y2-dy) || (x < x1+dx) || (x>x2-dx) || (y<y1+dy)))
)
{
// 設置數字顏色為綠色
gl_FragColor = vec4(0,1,0,1);
}
}
}
]]
}
運行截圖如下:
4 調試區(qū)設置為右上角

2 調試區(qū)設置為全屏

5 調試區(qū)設置為全屏

理論上來說, 有了上面這些基礎, 我們就可以自由地通過 shader 輸出要觀察變量的值了, 不過貌似有一個 bug: 重復運行 ledChar 函數會導致花屏, 暫時還沒搞清楚問題原因.
在顯示數字的方向上, 今天還想到幾種獲得數字字型的辦法, 一種是通過多個 vec4 或 mat4 來傳, 另一種是通過 texture 來傳, 大致考慮了下, 感覺還是通過 texture 傳比較簡單, 因為它是直接傳圖, 不需要再自己算像素點坐標了, 這個想法也準備后續(xù)試驗一下.
另外, 今天在有了這個思路之后, 去搜索了一下 shader debug, 結果在 StackOverflow 網站發(fā)現也有人問同樣的問題, 然后有人給出了本文提到的第一種簡單方法, 還有人提出一種很有想象力的方案, 那就是把要觀察的數據可視化, 直接用圖像來表達數字, 看著挺有趣, 點擊網頁鏈接, 準備后面也試試.
經過一番調試, 終于把剛剛能用的原型搞出來了:
OpenGL ES 2.0 Shader 調試新思路(二): 做一個可用的原型
浙公網安備 33010602011771號