??實(shí)現(xiàn)了GLSurfaceView繪制純色背景圖時(shí)楔绞,我們可以嘗試下實(shí)現(xiàn)如何渲染出一張圖片。
??這里需要簡(jiǎn)單介紹一個(gè)OpenGL的繪制原理计贰。
??基本概念
??在OpenGL中钦睡,任何事物都在3D空間中,而屏幕和窗口卻是2D像素?cái)?shù)組躁倒,這導(dǎo)致OpenGL的大部分工作都是關(guān)于把3D坐標(biāo)轉(zhuǎn)變?yōu)檫m應(yīng)你屏幕的2D像素荞怒。3D坐標(biāo)轉(zhuǎn)為2D坐標(biāo)的處理過程是由OpenGL的圖形渲染管線(Graphics Pipeline,大多譯為管線秧秉,實(shí)際上指的是一堆原始圖形數(shù)據(jù)途經(jīng)一個(gè)輸送管道褐桌,期間經(jīng)過各種變化處理最終出現(xiàn)在屏幕的過程)管理的。圖形渲染管線可以被劃分為兩個(gè)主要部分:第一部分把你的3D坐標(biāo)轉(zhuǎn)換為2D坐標(biāo)象迎,第二部分是把2D坐標(biāo)轉(zhuǎn)變?yōu)閷?shí)際的有顏色的像素荧嵌。這個(gè)教程里,我們會(huì)簡(jiǎn)單地討論一下圖形渲染管線砾淌,以及如何利用它創(chuàng)建一些漂亮的像素啦撮。
??圖形渲染管線接受一組3D坐標(biāo),然后把它們轉(zhuǎn)變?yōu)槟闫聊簧系挠猩?D像素輸出汪厨。圖形渲染管線可以被劃分為幾個(gè)階段赃春,每個(gè)階段將會(huì)把前一個(gè)階段的輸出作為輸入。所有這些階段都是高度專門化的(它們都有一個(gè)特定的函數(shù))劫乱,并且很容易并行執(zhí)行织中。正是由于它們具有并行執(zhí)行的特性锥涕,當(dāng)今大多數(shù)顯卡都有成千上萬(wàn)的小處理核心,它們?cè)贕PU上為每一個(gè)(渲染管線)階段運(yùn)行各自的小程序狭吼,從而在圖形渲染管線中快速處理你的數(shù)據(jù)层坠。這些小程序叫做著色器(Shader)。
??有些著色器允許開發(fā)者自己配置刁笙,這就允許我們用自己寫的著色器來替換默認(rèn)的破花。這樣我們就可以更細(xì)致地控制圖形渲染管線中的特定部分了,而且因?yàn)樗鼈冞\(yùn)行在GPU上采盒,所以它們可以給我們節(jié)約寶貴的CPU時(shí)間旧乞。OpenGL著色器是用OpenGL著色器語(yǔ)言(OpenGL Shading Language, GLSL)寫成的。
??我們繪制一個(gè)圖形最基本的需要倆個(gè)著色器磅氨,一個(gè)頂點(diǎn)著色器(Vertex Shader),一個(gè)片段著色器(Fragment Shader)嫡纠,OpenGL可以繪制點(diǎn)烦租,線,三角形除盏,所以我們想要繪制任何形狀的圖像都可以由這三種形狀拼接起來叉橱,例如我們想要一片方形的區(qū)域繪制圖像,那么我們就可以用倆個(gè)三角形拼接出這個(gè)方形使用者蠕。
??頂點(diǎn)著色器
??先看下頂點(diǎn)著色器窃祝,首先看下面一張圖
??頂點(diǎn)著色器的作用是繪制頂點(diǎn),踱侣,比如上面三角形的三個(gè)頂點(diǎn)粪小,我們想要繪制出這個(gè)三角形,那么我們只要知道三個(gè)頂點(diǎn)的坐標(biāo)就可以了抡句,需要注意的是探膊,繪制頂點(diǎn)的時(shí)候,頂點(diǎn)坐標(biāo)系是上右為正待榔,左下為負(fù)逞壁,交點(diǎn)為(0,0)锐锣,并且范圍是(-1腌闯,-1)到(1,1)之間雕憔,不可逾越姿骏,我們看下如何在代碼中創(chuàng)建頂點(diǎn)坐標(biāo)。
//聲明坐標(biāo)
float vertices[] = {
// 第一個(gè)三角形
1f, 1f, // 右上角
1f, -1f, // 右下角
-1f, 1f, // 左上角
// 第二個(gè)三角形
1f, -1f, // 右下角
-1f, -1f, // 左下角
-1f, 1f, // 左上角
};
private FloatBuffer vertexBuffer;
//分配空間
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
??使用數(shù)組來限定繪制范圍橘茉,下面來進(jìn)行內(nèi)存分配,上面寫了6個(gè)坐標(biāo)工腋,每個(gè)三角形三個(gè)坐標(biāo)姨丈,但是我們發(fā)現(xiàn)左上和右下的坐標(biāo)重復(fù)了,導(dǎo)致了額外的開銷擅腰,是否可以進(jìn)行簡(jiǎn)單化蟋恬,OpenGL提供了索引緩沖對(duì)象(EBO),我們可以避免寫這種重復(fù)坐標(biāo)的情況趁冈,這里借下萬(wàn)里大哥的圖一用歼争,如下。
??我們可以使用四個(gè)坐標(biāo)來表示倆個(gè)三角形渗勘,但是需要保證繪制倆個(gè)三角形的坐標(biāo)環(huán)繞方向一致沐绒,同時(shí)順時(shí)針或者同時(shí)逆時(shí)針,那么上面我們寫的六個(gè)坐標(biāo)我們就可以表示成如下:
private float[] vertexData = {
-1f, -1f, //A
1f, -1f, //B
-1f, 1f, //C
1f, 1f //D
};
??其中A旺坠,B乔遮,C表示第一個(gè)三角形,C取刃,B蹋肮,D表示第二個(gè)三角形,按照這種規(guī)則璧疗,可以避免寫重復(fù)的坐標(biāo)地址坯辩。
??片段著色器
??片段著色器的作用是繪制紋理,比如上面的三角形崩侠,我們繪制出了三角形漆魔,但是卻是空白一片的,那些磚塊一樣的紋理貼圖却音,就需要我們使用片段著色器給附著到這個(gè)空白三角形上改抡,我們?cè)倏聪孪聢D
??片段著色器和頂點(diǎn)著色器是不同的,片段著色器范圍是(0僧家,0)到(1雀摘,1),我們?cè)谑褂玫臅r(shí)候需要進(jìn)行區(qū)分八拱。
??我們想展示一張圖片阵赠,我們就需要用頂點(diǎn)著色器用倆個(gè)三角形拼接一個(gè)方形區(qū)域,并且將我們需要展示的圖片渲染在這片區(qū)域上就可以了肌稻。
??看下如何在代碼中聲明片段著色器:
//聲明坐標(biāo)
private float[] fragmentData = {
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
};
private FloatBuffer fragmentBuffer;
//分配空間
fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(fragmentData);
fragmentBuffer.position(0);
??和頂點(diǎn)著色器基本上沒有區(qū)別清蚀,也可以使用EBO的寫法。
??著色器程序
??寫好了倆個(gè)著色器后我們需要編寫著色器程序爹谭,作用就是如何使用坐標(biāo)并一些列操作枷邪,著色器程序我們也可以分成頂點(diǎn)著色器和片段著色器程序倆種,先看頂點(diǎn)著色器程序诺凡,看下圖:
attribute vec4 v_Position;
attribute vec2 f_Position;
varying vec2 ft_Position;
void main() {
ft_Position = f_Position;
gl_Position = v_Position;
}
??以上代碼使用GLSL(OpenGL Shader Language)編寫的东揣,聲明了三個(gè)變量践惑,一個(gè)四維向量和2倆個(gè)二維向量,包含了attribute嘶卧,vec4尔觉,varying等關(guān)鍵字,不知道GLSL語(yǔ)法的芥吟,可以先移步GLSL語(yǔ)法文檔侦铜,上面的v_Position我們用來表示頂點(diǎn)坐標(biāo),f_Position表示片段坐標(biāo)钟鸵,ft_Position用于頂點(diǎn)和片段之間的傳值臨時(shí)變量钉稍,gl_Position是GLSL的內(nèi)置變量,用于確定后的頂點(diǎn)坐標(biāo)棺耍,再看下片段著色器程序贡未,如下圖:
precision mediump float;
varying vec2 ft_Position;
uniform sampler2D sTexture;
void main() {
gl_FragColor=texture2D(sTexture, ft_Position);
}
??這里定義的ft_Position是由上面定義的頂點(diǎn)著色器程序中傳值過來的,所以定義類型需要用varying烈掠,gl_FragColor是GLSL的內(nèi)置變量羞秤,用于設(shè)置片段著色器的顏色。
??紋理
??當(dāng)我們想要把圖片等繪制到視圖上左敌,就要借助紋理來實(shí)現(xiàn),比如我們現(xiàn)在有一個(gè)空白相框俐镐,我們想要把我們和狗的合照放上去的時(shí)候矫限,我們可以直接放上去,那么就在空間上產(chǎn)生了一個(gè)位置佩抹,然后我們想放第二張照片的時(shí)候叼风,可以放在第一張照片的上面,那么此時(shí)對(duì)于相框來說棍苹,就需要用了倆個(gè)空間位置无宿,我們這里可以理解為倆個(gè)紋理,在使用紋理的時(shí)候枢里,我們需要獲得紋理的標(biāo)識(shí)孽鸡,稱之為紋理id(textureId),我們想要把一張圖片放上去就需要獲取一個(gè)紋理id栏豺,我們看下如下代碼:
int[] textureIds = new int[1];
//創(chuàng)建紋理空間
GLES20.glGenTextures(1, textureIds, 0);
textureid = textureIds[0];
//將紋理和視圖進(jìn)行綁定
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureid);
//激活紋理彬碱,默認(rèn)不激活
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//設(shè)置采樣器對(duì)應(yīng)具體的紋理單元,從0開始奥洼,依次遞增
GLES20.glUniform1i(sampler, 0);
//設(shè)置環(huán)繞方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
//設(shè)置過濾方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
//獲取圖片并展示成二級(jí)紋理
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.androids);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
//回收?qǐng)D片
bitmap.recycle();
//解綁紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
??利用大小為1的一維數(shù)組來表示一個(gè)紋理id巷疼,并且綁定到我們的視圖上,并且需要設(shè)置采樣器與我們使用的紋理單元一一對(duì)應(yīng)灵奖,這里我們只用來一個(gè)紋理嚼沿,片段著色器能訪問紋理對(duì)象估盘,但是我們?cè)鯓幽馨鸭y理對(duì)象傳給片段著色器呢?GLSL有一個(gè)供紋理對(duì)象使用的內(nèi)建數(shù)據(jù)類型骡尽,叫做采樣器(Sampler)遣妥,它以紋理類型作為后綴,比如sampler1D爆阶、sampler3D燥透,這里用了一個(gè)采樣器sampler,sampler的聲明在GLSL代碼中辨图,在上面的代碼中sTexture即是班套,當(dāng)我們使用完了紋理后,我們不使用時(shí)故河,仍然調(diào)用glBindTexture方法進(jìn)行解綁吱韭,只是傳值改成了0,中間一部分代碼鱼的,我們看下:
//設(shè)置環(huán)繞方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
??紋理環(huán)繞方式
??紋理坐標(biāo)的范圍通常是從(0, 0)到(1, 1)理盆,那如果我們把紋理坐標(biāo)設(shè)置在范圍之外會(huì)發(fā)生什么?OpenGL默認(rèn)的行為是重復(fù)這個(gè)紋理圖像(我們基本上忽略浮點(diǎn)紋理坐標(biāo)的整數(shù)部分)凑阶,但OpenGL提供了更多的選擇
??當(dāng)紋理坐標(biāo)超出默認(rèn)范圍時(shí)猿规,每個(gè)選項(xiàng)都有不同的視覺效果輸出。我們來看看這些紋理圖像的例子:
還有一部分代碼宙橱,如下:
//設(shè)置過濾方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
??紋理過濾
??紋理坐標(biāo)不依賴于分辨率(Resolution)姨俩,它可以是任意浮點(diǎn)值,所以O(shè)penGL需要知道怎樣將紋理像素(Texture Pixel师郑,也叫Texel环葵,譯注1)映射到紋理坐標(biāo)。當(dāng)你有一個(gè)很大的物體但是紋理的分辨率很低的時(shí)候這就變得很重要了宝冕。你可能已經(jīng)猜到了张遭,OpenGL也有對(duì)于紋理過濾(Texture Filtering)的選項(xiàng)。紋理過濾有很多個(gè)選項(xiàng)地梨,但是現(xiàn)在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR菊卷。
??GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認(rèn)的紋理過濾方式湿刽。當(dāng)設(shè)置為GL_NEAREST的時(shí)候的烁,OpenGL會(huì)選擇中心點(diǎn)最接近紋理坐標(biāo)的那個(gè)像素。下圖中你可以看到四個(gè)像素诈闺,加號(hào)代表紋理坐標(biāo)渴庆。左上角那個(gè)紋理像素的中心距離紋理坐標(biāo)最近,所以它會(huì)被選擇為樣本顏色:
??GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會(huì)基于紋理坐標(biāo)附近的紋理像素襟雷,計(jì)算出一個(gè)插值刃滓,近似出這些紋理像素之間的顏色。一個(gè)紋理像素的中心距離紋理坐標(biāo)越近耸弄,那么這個(gè)紋理像素的顏色對(duì)最終的樣本顏色的貢獻(xiàn)越大咧虎。下圖中你可以看到返回的顏色是鄰近像素的混合色:
??那么這兩種紋理過濾方式有怎樣的視覺效果呢?讓我們看看在一個(gè)很大的物體上應(yīng)用一張低分辨率的紋理會(huì)發(fā)生什么吧(紋理被放大了计呈,每個(gè)紋理像素都能看到):
??設(shè)置好這些砰诵,我們最后執(zhí)行
//獲取圖片并展示成二級(jí)紋理
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.androids);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
??就可以把圖片渲染在指定的二級(jí)紋理上了。
??當(dāng)然捌显,最終如果需要真正的展示出我們的圖片茁彭,我們需要把我們的頂點(diǎn)著色器和片段著色器全部給繪制出來才可以。
??查看以下代碼:
String vertexSource = ShaderUtil.getRawResource(context, R.raw.vertex_shader);
String fragmentSource = ShaderUtil.getRawResource(context, R.raw.fragment_shader);
program = ShaderUtil.createProgram(vertexSource, fragmentSource);
vPosition = GLES20.glGetAttribLocation(program, "v_Position");
fPosition = GLES20.glGetAttribLocation(program, "f_Position");
sampler = GLES20.glGetUniformLocation(program, "sTexture");
...
...
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1f, 0f, 0f, 1f);
//使用編譯成功的程序鏈
GLES20.glUseProgram(program);
//綁定二級(jí)紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureid);
//激活頂點(diǎn)坐標(biāo)
GLES20.glEnableVertexAttribArray(vPosition);
//告知OpenGL如何采樣紋理
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8,
vertexBuffer);
GLES20.glEnableVertexAttribArray(fPosition);
GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8,
fragmentBuffer);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
??編譯完我們的頂點(diǎn)著色器代碼和片段著色器代碼扶歪,設(shè)置了畫板的清除顏色并且使用了倆個(gè)著色器生成的程序鏈理肺,然后綁定二級(jí)紋理并且激活坐標(biāo)設(shè)置采樣規(guī)則,頂點(diǎn)和片段著色器流程類似善镰,最后調(diào)用glDrawArrays方法妹萨,從0偏移開始,繪制四個(gè)坐標(biāo)炫欺,也就是倆個(gè)三角形合起來的方形乎完,同學(xué)們可以可以嘗試更改這倆個(gè)數(shù)字查看效果。
??到此我們的渲染圖片紋理部分就完成了品洛,給出這節(jié)課涉及到的代碼:
public class TextureRender implements WLEGLSurfaceView.WlGLRender {
private Context context;
private float[] vertexData = {
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
private FloatBuffer vertexBuffer;
private float[] fragmentData = {
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
};
private FloatBuffer fragmentBuffer;
private int program;
private int vPosition;
private int fPosition;
private int textureid;
private int sampler;
public TextureRender(Context context) {
this.context = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(fragmentData);
fragmentBuffer.position(0);
}
@Override
public void onSurfaceCreated() {
String vertexSource = ShaderUtil.getRawResource(context, R.raw.vertex_shader);
String fragmentSource = ShaderUtil.getRawResource(context, R.raw.fragment_shader);
program = ShaderUtil.createProgram(vertexSource, fragmentSource);
vPosition = GLES20.glGetAttribLocation(program, "v_Position");
fPosition = GLES20.glGetAttribLocation(program, "f_Position");
sampler = GLES20.glGetUniformLocation(program, "sTexture");
int[] textureIds = new int[1];
//創(chuàng)建紋理空間
GLES20.glGenTextures(1, textureIds, 0);
textureid = textureIds[0];
//將紋理和視圖進(jìn)行綁定
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureid);
//激活紋理囱怕,默認(rèn)不激活
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//設(shè)置采樣器對(duì)應(yīng)具體的紋理單元,從0開始毫别,依次遞增
GLES20.glUniform1i(sampler, 0);
//設(shè)置環(huán)繞方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
//設(shè)置過濾方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
//獲取圖片并展示成二級(jí)紋理
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.androids);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
//回收?qǐng)D片
bitmap.recycle();
//解綁紋理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
@Override
public void onSurfaceChanged(int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame() {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1f, 0f, 0f, 1f);
GLES20.glUseProgram(program);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureid);
GLES20.glEnableVertexAttribArray(vPosition);
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8,
vertexBuffer);
GLES20.glEnableVertexAttribArray(fPosition);
GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8,
fragmentBuffer);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
}
??說的很多,代碼其實(shí)并不復(fù)雜典格,看下如何使用:
public class GLTextureView extends EGLSurfaceView{
public GLTextureView(Context context) {
this(context, null);
}
public GLTextureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public GLTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setRender(new TextureRender(context));
}
}
??GLTextureView在上一小節(jié)已經(jīng)介紹過岛宦,這節(jié)課結(jié)合上節(jié)內(nèi)容一起使用,即可完成圖片的渲染耍缴。