OpenGL ES 2.0 Shader 調試新思路(二): 做一個可用的原型
目錄
背景介紹
請參考前文OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式
優化 ledChar 函數
前文中我們寫了一個可以用來顯示 1~0
10個數字字型的函數, 不過回頭看看, 發現這個函數有些不太好閱讀, 為方便討論, 把該函數的代碼拷貝在下面:
void ledChar(int,float,float,float,float);
// 構造數字
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;
// 設定調試區顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區背景色
gl_FragColor = vec4(0.2,0.2,0.8,.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,.5);
}
}
}
代碼完成后, 看着復雜的判斷條件, 參差不齊的格式, 覺得不太好閱讀, 忽然想到另一種實現 LED
字型的算法, 我們上面的算法是計算7段數碼管每段的坐標范圍, 然后繪制, 最多需要繪制5段(數字2,5,6,8,9), 判斷語句寫起來比較長, 其實我們可以反其道而行之, 意思就是說不畫數字筆畫, 改畫數字筆畫間的矩形, 因為仔細分析一下每個數字字型, 就會發現每個數字都可以用1個或2個小矩形分割出來, 上個示意圖, 第一個示意圖是傳統的繪制方式, 第二個示意圖是我們剛剛想到的用"矩形掩碼", 或者叫"蒙版", 總之就這個意思:
傳統LED數字繪制原理圖
VS. 新想到的利用"矩形掩碼"繪制原理圖
:
截圖:
為了方便起見, 寫了一個計算矩形區域的輔助函數 inRect
, 我們把新寫的函數命名為 ledRectChar
, 代碼如下:
float x = vTexCoord.x;
float y = vTexCoord.y;
void ledRectChar(int,float,float,float,float);
bool inRect(float,float,float,float);
bool inRect(float x1,float x2, float y1, float y2){
if(x>x1 && x<x2 && y>y1 && y<y2) { return true; } else { return false; }
}
void ledRectChar(int n, float xa,float xb, float ya, float yb){
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;
// 設定調試區顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區背景色為綠色
gl_FragColor = vec4(0.2,1.0,0.2,1.0);
// 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
(num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
(num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
(num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
(num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
(num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy))
)
{
gl_FragColor = vec4(0,0,0,.5);
}
}
}
這樣考慮角度一變, 就發現其實用矩形在這種場景下更簡單, 而且代碼看起來清楚多了, 數字 7
和 0
用了一個矩形, 其他數字都用兩個矩形就"掩"出來了(其實1
用1個矩形就可以, 用兩個是為了更美觀一些)
.
另外, 貌似使用了太多的函數, 導致效率不高, 其實我也很樂意把這些函數全部都寫成宏, 只是不太會寫帶參數的宏, 試了半天 inRect
, 比如
// ! 說明, 這是錯的, 編譯不通過
#define inRect(x1,x2,y1,y2) x>(x1)&&x<(x2)&&y>(y1)&&y<(y2)?true:false
結果老是有錯誤, 就沒繼續研究了(后來仔細研究了 OpenGL Shader Language
之后才發現它不允許帶參數的宏).
改進為可用的原型
用了改進版的 ledRectChar
作為基礎函數, 我們開始考慮實際的使用場景, 實際編程過程中 shader
用到的變量的值肯定不會只是一個一位整數, 所以我們首先得考慮多位整數, 其次還要考慮浮點數, 另外還要考慮負數的表示, 最后要考慮的是表示范圍和准確度(這點最麻煩, 本文只是大致說一下).
列一下后續的需求清單:
- 多位正整數
- 多位浮點數
- 負數
- 給出表示范圍和准確度
接下來我們一項一項來
表示多位正整數
在我發出前文 OpenGL ES 2.0 Shader 調試新思路(一): 改變提問方式 后, 論壇上的一位朋友 @dave1707
用我們的基礎函數 ledChar
寫了一段表示多位正整數的代碼, 並建議我把它完善一下, 首先表示感謝, 他的代碼如下:
highp int nbr=8293; // number to display
float m=0.96;
while (nbr>0)
{ m=m-0.015;
int nn=nbr-((nbr/10)*10);
ledChar(nn, m, 0.01, 0.96, 0.01);
nbr=nbr/10;
}
也就是說多位正整數的需求已經解決, 這里存在一個存儲精度的問題, 也就是當要表示的數字大於某個值時就會導致溢出, 這種情況我們不做太多處理, 主要是因為這里處理起來比較麻煩, 我們會在注釋中說明本函數適用的數字范圍.
接下來我們會在他的代碼的基礎上繼續前進, 我們先分析一下 多位浮點數
和 負數
這兩個需求, 發現它們一個需要 小數點
, 一個需要負號
, 也就是說在我們的基礎函數 ledRectChar
中需要新增兩種字型, 那么我們先來升級一下基礎函數.
新增的兩種字型:小數點和負號
先來處理小數點
, 前面用變量 num
的值 1~0
分別表示 1~0
這10個數字的字型, 那么新增的小數點
和負號分別用數字 10
和 11
表示, 然后用"矩形掩碼"把它們的字型畫出.
增加這么兩行判斷:
(num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
(num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy)))
更新后的 ledRectChar
函數如下:
void ledRectChar(int n, float xa,float xb, float ya, float yb){
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;
// 設定調試區顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區背景色
gl_FragColor = vec4(0.2,1.0,0.2,1.0);
// 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
(num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
(num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
(num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
(num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
(num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy)) ||
// 傳入10則繪制小數點, 傳入11則繪制負號, 傳入12則清空
(num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
(num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy))) ||
(num==12)
)
{
gl_FragColor = vec4(0,0,0,.5);
}
}
}
浮點數和負數
調試通過, OK, 現在基礎函數已經可以提供全部字型了, 我們接着來實現浮點數的表示, 基於前面 @dave1707
的代碼, 把浮點數先取絕對值(為了避免要區分正負數分別對應的floor
和ceil
兩種取整方式), 再把絕對值分離出整數部分和小數部分, 然后都當做整數來處理, 按個十百千萬...
位插入一個20位的數組, 整數部分和小數部分中間插一個小數點
所對應的數字10
, 最后判斷一下是不是負數, 是的話就插入負號
所對應的數字11
, 還有就是開始要初始化一下數組, 初始值置為 12
(在基礎函數中對應黑色背景), 否則數組默認值都是 0
, 顯示時會在空白的數字位全部顯示為 0
, 影響觀感, 代碼如下:
void showFloat(float f){
int myNum[20];
int k = 0;
int iPart = int(floor(abs(f)));
int fPart = int(fract(abs(f))*100000.0);
float m=0.86;
// 初始化數組,全部置為代表黑色的12
for(int i=0; i<20; i++){
myNum[i] = 12;
}
// 插入小數部分
while (fPart>0)
{
// 從個位開始, 依次取出個位,十位,百位,千位...的數字值
myNum[k++]=fPart-((fPart/10)*10);
fPart=fPart/10;
}
// 如果是0
if(f==0.0){myNum[k++] = 0;}
// 插入小數點
myNum[k++] = 10;
// 插入整數部分
while (iPart>0)
{
myNum[k++]=iPart-((iPart/10)*10);
iPart=iPart/10;
}
// 如果是負數,則插入代表負號的11
if(f<0.0) { myNum[k++]=11;}
// 循環輸出數字數組
for(int i=0; i<20; i++)
{
m = m-0.03;
ledRectChar(myNum[i], m, 0.02, 0.6, 0.15);
}
}
很好, 調試通過, 基本搞定, 好像忘記處理負整數了, 為了避免麻煩, 我們可以建議用戶把負整數
進行強制類型轉換為負浮點數
, 就可以直接使用我們的 showFloat
函數了, 具體來說就是這么調用:
- showFloat(float(-1234));
顯示截圖如下:
顯示范圍和准確度
最后說一下這個不得不說的問題, 很多編程語言都需要考慮一個數值表示范圍, 尤其是浮點數, 比如 shader
里的浮點數就是數值越大, 小數位越少, 而且這時比較小的小數會被舍掉, 我們為小數部分留了5位, 整數部分留了13位, 當然, 如果你需要調試更大的數, 也可以自己修改數組的大小--不過好像 shader
中太大的數會返回溢出, 大家根據自己的需求看着辦吧.
看看這幾個截圖:
- showFloat(2097152.411);
- showFloat(2097152.11);
在此不得不贊嘆一下我大 Common Lisp
的強悍, 畢竟能直接計算 1024^1024
(1024的1024次方)的語言唯有我大 Common Lisp
了, 看看:
截圖:
可用原型的完整代碼
現在我們基本完成一個可用原型了, 雖然效率不怎么樣, FPS
甚至降低了20倍(從60降低到3), 但是首先我們解決了有沒有的問題, 好不好的問題就留待后面解決了, 如果有需求那就繼續優化好了, 下面給出可用原型的全部代碼:
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;
float x = vTexCoord.x;
float y = vTexCoord.y;
void ledChar(int,float,float,float,float);
void ledRectChar(int,float,float,float,float);
void showInt(int);
void showFloat(float);
bool inRect(float,float,float,float);
void main() {
lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
// 默認全部設置為黑色
gl_FragColor = vec4(.1,.1,.1,1);
showFloat(-.1111111);
//showFloat(float(-9765));
}
void showFloat(float f){
int myNum[20];
int k = 0;
int iPart = int(floor(abs(f)));
int fPart = int(fract(abs(f))*100000.0);
float m=0.86;
// 初始化數組,全部置為代表黑色的12
for(int i=0; i<20; i++){
myNum[i] = 12;
}
// 插入小數部分
while (fPart>0)
{
// 從個位開始, 依次取出個位,十位,百位,千位...的數字值
myNum[k++]=fPart-((fPart/10)*10);
fPart=fPart/10;
}
// 如果是0
if(f==0.0){myNum[k++] = 0;}
// 插入小數點
myNum[k++] = 10;
// 插入整數部分
while (iPart>0)
{
myNum[k++]=iPart-((iPart/10)*10);
iPart=iPart/10;
}
// 如果是負數,則插入代表負號的11
if(f<0.0) { myNum[k++]=11;}
// 循環輸出數字數組
for(int i=0; i<20; i++)
{
m = m-0.03;
ledRectChar(myNum[i], m, 0.02, 0.6, 0.15);
}
}
bool inRect(float x1,float x2, float y1, float y2){
if(x>x1 && x<x2 && y>y1 && y<y2) { return true; } else { return false; }
}
void ledRectChar(int n, float xa,float xb, float ya, float yb){
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;
// 設定調試區顯示范圍
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// 設置調試區背景色
gl_FragColor = vec4(0.2,1.0,0.2,1.0);
// 分別繪制出 LED 形式的數字 1~0 , 用黑色繪制1個或2個矩形,由矩形以外的綠色區域組成字型
if((num==1 && (inRect(x1,ox-dx,y1,y2) || inRect(ox+dx,x2,y1,y2))) ||
(num==2 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2,y1+dy,oy-dy/2.0))) ||
(num==3 && (inRect(x1,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==4 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2) || inRect(x1,x2-dx,y1,oy-dy/2.0))) ||
(num==5 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==6 && (inRect(x1+dx,x2,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy))) ||
(num==7 && inRect(x1,x2-dx,y1,y2-dy)) ||
(num==8 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1+dx,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==9 && (inRect(x1+dx,x2-dx,oy+dy/2.0,y2-dy) || inRect(x1,x2-dx,y1+dy,oy-dy/2.0))) ||
(num==0 && inRect(x1+dx,x2-dx,y1+dy,y2-dy)) ||
// 傳入10則繪制小數點, 傳入11則繪制負號, 傳入12則清空
(num==10 && (inRect(x1,x2,oy-dy,y2) || inRect(x1,ox-dx*2.0,y1,oy-dy) || inRect(ox+dx*2.0,x2,y1,oy-dy) )) ||
(num==11 && (inRect(x1,x2,oy+dy,y2) || inRect(x1,x2,y1,oy-dy))) ||
(num==12)
)
{
gl_FragColor = vec4(0,0,0,.5);
}
}
}
]]
}
配套Codea代碼
-- Shader debug
displayMode(OVERLAY)
function setup()
m = mesh()
m:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
m.shader = shader(myShader.vsBase,myShader.fsBase)
-- 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
下調試 shader
程序起碼有個工具勉強可用了. 當然, 這幾個函數也可以用於調試其他平台的 shader
程序.
為了提高人類整體的工作效率, 我們后續會把這個原型發布到 Github
上, 以供其他需要觀察 shader
內部變量的同學使用, 起個響亮的名字 ShaderDebugger
: