本節我們在上一節的基礎上繼續添加地形圖功能,我們要分析的目標就是《OpenGL ES應用開發實踐指南 Android卷》書中第12章實現的最終的結果,代碼下載請點擊:Opengl ES Source Code,該Git庫中的heightmap Module就是我們本節要分析的目標,先看下本節最終實現的結果。
可以看到,地形圖中有高有低,是用綠色來表示的,最接近底部的顏色最深,最上面的最淺。我們來看一下本節的代碼,本節的代碼還是在前一節的基礎上添加新功能來實現的,所以如果對之前的章節內容有不清楚的地方,請大家先回頭搞清楚之前的邏輯:Opengl ES系列學習--用粒子增添趣味、Opengl ES系列學習--增加天空盒。
我們還是隻看差異的部分,目錄結構圖如下:
data包下新增了IndexBuffer、VertexBuffer兩個類,這兩個類是爲了包裝頂點緩衝區和索引緩衝區而定義的,先來看VertexBuffer頂點緩衝區。
頂點緩衝區全稱是GL_ARB_vertex_buffer_object,擴展可以提升OpenGL的性能。它提供了頂點數組和顯示列表,有效避免了低效實現這些功能。Vertex Buffer Object(VBO) 允許頂點數據儲存在高性能顯卡上,即服務端的內存中,改善數據傳輸效率。如果緩衝區對象保存了像素數據,它就被稱做Pixel Buffer Object(PBO)。使用頂點數據可以減少函數調用次數及複用共享頂點,然而,頂點數組的缺點是頂點函數及頂點數據在客戶端(對於OpenGL來說,顯卡爲服務端,其它爲客戶端),每次引用頂點數組時,都必須將頂點數據從客戶端(內存)發送到服務端(顯卡)。另一方面,顯示列表是服務端的函數,它不會再重頭傳送數據。但是,一旦顯示列表被編譯了,顯示列表中的數據就不能修改了。
Vertex buffer object (VBO) 爲頂點創建創建了一個緩衝區對象。緩衝區對象在服務端的高性能內存中,並提供了相同的函數,引用這些數組,如glVertexPointer()、glNormalPointer()、 glTexCoordPointer()等等。頂點緩衝區內存管理器將緩衝區對象放在儲存器中最佳的位置。這依賴了用戶輸入的模式:“target”模式和“usage”模式。因此,儲存管理器可以優化緩衝區,平衡三種內存:system、AGP、video memory。與顯示列表不同的是,在頂點緩衝區對象中的數據可以讀也可以將它映射到服務端的內存空間中,然後更新它的數據。
VBO另一個重要的優點是,可以在許多客戶端中共享緩衝區對象,就像顯示列表和紋理那樣。由於VBO在服務端,多個客戶端可以通過對應的標識符訪問同一個緩衝區。
明白了VBO的優化,我們來看一下如何使用它,VertexBuffer類的源碼如下:
public class VertexBuffer {
private final int bufferId;
public VertexBuffer(float[] vertexData) {
// Allocate a buffer.
final int buffers[] = new int[1];
glGenBuffers(buffers.length, buffers, 0);
if (buffers[0] == 0) {
throw new RuntimeException("Could not create a new vertex buffer object.");
}
bufferId = buffers[0];
// Bind to the buffer.
glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
// Transfer data to native memory.
FloatBuffer vertexArray = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexArray.position(0);
// Transfer data from native memory to the GPU buffer.
glBufferData(GL_ARRAY_BUFFER, vertexArray.capacity() * BYTES_PER_FLOAT,
vertexArray, GL_STATIC_DRAW);
// IMPORTANT: Unbind from the buffer when we're done with it.
glBindBuffer(GL_ARRAY_BUFFER, 0);
// We let vertexArray go out of scope, but it won't be released
// until the next time the garbage collector is run.
}
public void setVertexAttribPointer(int dataOffset, int attributeLocation,
int componentCount, int stride) {
glBindBuffer(GL_ARRAY_BUFFER, bufferId);
// This call is slightly different than the glVertexAttribPointer we've
// used in the past: the last parameter is set to dataOffset, to tell OpenGL
// to begin reading data at this position of the currently bound buffer.
glVertexAttribPointer(attributeLocation, componentCount, GL_FLOAT,
false, stride, dataOffset);
glEnableVertexAttribArray(attributeLocation);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
}
成員變量只有一個:bufferId,它表示我們向GPU申請創建的頂點緩衝區的ID,構造方法中傳入的float[] vertexData參數就表示我們要繪製的頂點數組,當中所有的邏輯就是VBO的使用流程,先調用glGenBuffers向GPU申請一塊頂點緩衝區,第二個參數buffers是一個輸出參數,表示GPU爲我們創建好的VBO的ID,創建成功,則ID值會寫入到該數組中;接着調用glBindBuffer以GL_ARRAY_BUFFER類型綁定頂點緩衝區;然後創建FloatBuffer用來存儲頂點數據,因爲我們要存儲的是float類型,所以分配的內存長度需要乘BYTES_PER_FLOAT(4),創建成功並把頂點數據存儲進去;然後調用glBufferData給頂點緩衝區填充值,註釋也寫的很清楚了,最後調用glBindBuffer並傳入參數0解綁,這一句必須要有,否則我們其他地方的邏輯可能無法正常工作。glBufferData函數的詳細說明如下:
接着再來看setVertexAttribPointer方法,和之前VertexArray中的該方法的實現不同,我們已經在構造方法中將頂點數值傳遞到GPU分配的內存中了,所以這裏調用glVertexAttribPointer時,最後一個參數不是Buffer類型,我們只需要給API傳遞stride和offset參數,告訴它從哪裏開始取值就可以了。調用流程還是先綁定,再賦值,然後打開頂點屬性,最後解綁。看完該類的代碼,我們就需要明白頂點緩衝區要如何使用了。
接着來看IndexBuffer索引緩衝區,源碼如下:
public class IndexBuffer {
private final int bufferId;
public IndexBuffer(short[] indexData) {
// Allocate a buffer.
final int buffers[] = new int[1];
glGenBuffers(buffers.length, buffers, 0);
if (buffers[0] == 0) {
throw new RuntimeException("Could not create a new index buffer object.");
}
bufferId = buffers[0];
// Bind to the buffer.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers[0]);
// Transfer data to native memory.
ShortBuffer indexArray = ByteBuffer
.allocateDirect(indexData.length * BYTES_PER_SHORT)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(indexData);
indexArray.position(0);
// Transfer data from native memory to the GPU buffer.
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexArray.capacity() * BYTES_PER_SHORT,
indexArray, GL_STATIC_DRAW);
// IMPORTANT: Unbind from the buffer when we're done with it.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
// We let the native buffer go out of scope, but it won't be released
// until the next time the garbage collector is run.
}
public int getBufferId() {
return bufferId;
}
}
索引緩衝區和我們上一節中繪製天空盒時指定天空盒六個面所使用的索引值的意思基本相同,它是用來描述我們繪製高度圖所要繪製的所有三角形每個頂點的索引值,這裏一定要理解清楚,它不是真實的頂點數據,而只是對所有頂點數據指定的一個索引,用它來指示着色器程序繪製所有頂點時,要如何取頂點!!這裏如果有含糊的地方,一定要停下來,搞清楚頂點和索引的關係!!
這裏有個問題,爲什麼索引緩衝區中存儲的數值中使用short,而不是int或者float呢?試着把代碼修改爲int,效果如下:
而如果修改爲float,則屏幕全黑,什麼也繪製不出來。這裏始終沒明白使用short的原因,如果有哪位朋友明白的話,請告訴一下。該類中的邏輯就不詳細分析了,和VertexBuffer基本相同,只是它在對Buffer進行操作時,因爲是索引緩衝區,所以類型爲GL_ELEMENT_ARRAY_BUFFER。
接下來看objects包,該包下新增了Heightmap類,其他的完全相同。Heightmap類就是爲了繪製高度圖定義的,源碼如下:
public class Heightmap {
private static final int POSITION_COMPONENT_COUNT = 3;
private final int width;
private final int height;
private final int numElements;
private final VertexBuffer vertexBuffer;
private final IndexBuffer indexBuffer;
public Heightmap(Bitmap bitmap) {
width = bitmap.getWidth();
height = bitmap.getHeight();
if (width * height > 65536) {
throw new RuntimeException("Heightmap is too large for the index buffer.");
}
numElements = calculateNumElements();
vertexBuffer = new VertexBuffer(loadBitmapData(bitmap));
indexBuffer = new IndexBuffer(createIndexData());
}
/**
* Copy the heightmap data into a vertex buffer object.
*/
private float[] loadBitmapData(Bitmap bitmap) {
final int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
bitmap.recycle();
final float[] heightmapVertices =
new float[width * height * POSITION_COMPONENT_COUNT];
int offset = 0;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
// The heightmap will lie flat on the XZ plane and centered
// around (0, 0), with the bitmap width mapped to X and the
// bitmap height mapped to Z, and Y representing the height. We
// assume the heightmap is grayscale, and use the value of the
// red color to determine the height.
final float xPosition = ((float) col / (float) (width - 1)) - 0.5f;
final float yPosition =
(float) Color.red(pixels[(row * height) + col]) / (float) 255;
final float zPosition = ((float) row / (float) (height - 1)) - 0.5f;
heightmapVertices[offset++] = xPosition;
heightmapVertices[offset++] = yPosition;
heightmapVertices[offset++] = zPosition;
}
}
return heightmapVertices;
}
private int calculateNumElements() {
// There should be 2 triangles for every group of 4 vertices, so a
// heightmap of, say, 10x10 pixels would have 9x9 groups, with 2
// triangles per group and 3 vertices per triangle for a total of (9 x 9
// x 2 x 3) indices.
return (width - 1) * (height - 1) * 2 * 3;
}
/**
* Create an index buffer object for the vertices to wrap them together into
* triangles, creating indices based on the width and height of the
* heightmap.
*/
private short[] createIndexData() {
final short[] indexData = new short[numElements];
int offset = 0;
for (int row = 0; row < height - 1; row++) {
for (int col = 0; col < width - 1; col++) {
// Note: The (short) cast will end up underflowing the number
// into the negative range if it doesn't fit, which gives us the
// right unsigned number for OpenGL due to two's complement.
// This will work so long as the heightmap contains 65536 pixels
// or less.
short topLeftIndexNum = (short) (row * width + col);
short topRightIndexNum = (short) (row * width + col + 1);
short bottomLeftIndexNum = (short) ((row + 1) * width + col);
short bottomRightIndexNum = (short) ((row + 1) * width + col + 1);
// Write out two triangles.
indexData[offset++] = topLeftIndexNum;
indexData[offset++] = bottomLeftIndexNum;
indexData[offset++] = topRightIndexNum;
indexData[offset++] = topRightIndexNum;
indexData[offset++] = bottomLeftIndexNum;
indexData[offset++] = bottomRightIndexNum;
}
}
return indexData;
}
public void bindData(HeightmapShaderProgram heightmapProgram) {
vertexBuffer.setVertexAttribPointer(0,
heightmapProgram.getPositionAttributeLocation(),
POSITION_COMPONENT_COUNT, 0);
}
public void draw() {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer.getBufferId());
glDrawElements(GL_TRIANGLES, numElements, GL_UNSIGNED_SHORT, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
}
先來看一下成員變量,POSITION_COMPONENT_COUNT = 3還是和之前一樣,表示要描述一個頂點位置,需要3個size;width表示高度圖的寬度;height表示高度圖的高度;numElements表示要繪製所有的頂點三解形需要多少個索引值,這是什麼意思呢?需要詳細解釋一下,大家請下圖。
假如我們有3 * 3個頂點,那麼每四個頂點分爲一組,一共需要四個矩形((3 - 1)*(3 - 1)= 4)、八個三角形(4 * 2 = 8)才能完全畫出來,每個三角形需要三個索引值來表示,一共需要24個索引值(8 * 3 = 24)。而加載進來的高度圖,每個像素都對應一個頂點,那麼總共需要(width - 1)*(height - 1)* 2 * 3個索引值才能完全描述,這就是calculateNumElements方法的含義了;vertexBuffer就定義了所有頂點值,是通過對Bitmap進行轉換獲取的;indexBuffer是索引數據,需要轉換爲索引緩衝區,這裏一定要把它和頂點緩衝區分開。假如上面這3 * 3個頂點(序號依次爲1--9),我們把它分爲四組,四個矩形,要用索引緩衝區來描述的話,那應該是[1,2,4,5]、[2,3,5,6]、[4,5,7,8]、[5,6,8,9]。
接下來的構造方法邏輯非常清晰,調用成員方法給成員變量賦值,我們重點要看的就是它剩下的成員方法。
先看loadBitmapData,入參爲加載進來的Bitmap,這裏還是要多說一句,實質性的頂點數組是根據Bitmap生成的,而索引數據不是真正要用來繪製高度圖的,只表示我們要按照如何的順序去頂點緩衝區中取頂點值。深刻理解了這兩個的差別,我們就清楚了,因爲loadBitmapData方法的返回值是用作參數來生成vertexBuffer的,所以它的作用肯定是生成原始的頂點數據了。先創建一個width * height大小的數組,然後調用bitmap.getPixels方法讀取高度圖中每個像素點的色值,然後創建一個width * height * POSITION_COMPONENT_COUNT大小的數組,用來存儲真正的頂點數據;接着兩個for循環的意思大家應該都清楚,因爲bitmap.getPixels返回的是一維數組,我們要用width和height將它轉換爲二維數組;for循環中xPosition和zPosition的值和每個點的像素數據完全無關,直接是用width和height計算出來的,減去0.5f的意思就是把像素點映射在X—Z平面上,比如最左上角的第一個點,它的col、row都爲0,得到的xPosition和zPosition都是-0.5f,對應在X—Z平面最左上角,而最右下角的一個點,它的col等於width,row等於height,得到的xPosition和zPosition都是0.5f,對應在X—Z平面最右下角;yPosition的取值就和像素點的色值相關了,取該像素點的紅色分量,然後除以255進行歸一化,所以這個結果的意思就是說最後的高度圖是按照紅色分量的值排布的了!!!獲取到一個頂點的xPosition、yPosition、zPosition值,然後將它填充到頂點數組中。
calculateNumElements方法的意思我們已經講過了;繼續看createIndexData方法,先根據計算出的numElements創建索引數組,然後根據我們上圖畫的矩形取到索引值,就是左上、右上、左下、右下,然後使用這四個點描述三角形的捲曲順序,後面我們會看到調用glEnable(GL_CULL_FACE)打開了Opengl的剔除功能,剔除功能的意思是指,如果三角形的捲曲順序是逆時針,則它就被繪製出來,否則就會被丟棄,所以我們這裏描述一個三角形的時間,一定要按照逆時針的順序來,如下圖。
bindData方法是用來給着色器中的a_Position屬性賦值的;draw方法就直接以GL_TRIANGLES繪製三角形的方式繪製索引索引緩衝區了,因爲在Heightmap的構造方法中,創建頂點緩衝區VertexBuffer、索引緩衝區IndexBuffer時,已經調用Opengl ES的API把頂點數據和索引數據傳入到GPU當中了,所以這裏就不需要傳遞數據了。
好,接着看HeightmapShaderProgram,源碼如下:
public class HeightmapShaderProgram extends ShaderProgram {
private final int uMatrixLocation;
private final int aPositionLocation;
public HeightmapShaderProgram(Context context) {
super(context, R.raw.heightmap_vertex_shader,
R.raw.heightmap_fragment_shader);
uMatrixLocation = glGetUniformLocation(program, U_MATRIX);
aPositionLocation = glGetAttribLocation(program, A_POSITION);
}
public void setUniforms(float[] matrix) {
glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
}
public int getPositionAttributeLocation() {
return aPositionLocation;
}
}
高度圖着色器中的代碼比較簡單,它還是繼承父類ShaderProgram,所以我們可以想象,到這裏已經有三個着色器程序了,每個着色器程序對應一個頂點着色器和一個片段着色器,那就一共就有六個glsl着色器了。它當中就是取到uMatrixLocation矩陣、aPositionLocation的值,然後通過setUniforms給矩陣賦值,通過提供aPositionLocation的get方法然後它,然後再調用API給它賦值了。
接着看ParticlesRenderer,我們只分析它當中不同的地方,整個類的源碼如下:
public class ParticlesRenderer implements Renderer {
private final Context context;
private final float[] modelMatrix = new float[16];
private final float[] viewMatrix = new float[16];
private final float[] viewMatrixForSkybox = new float[16];
private final float[] projectionMatrix = new float[16];
private final float[] tempMatrix = new float[16];
private final float[] modelViewProjectionMatrix = new float[16];
private HeightmapShaderProgram heightmapProgram;
private Heightmap heightmap;
private SkyboxShaderProgram skyboxProgram;
private Skybox skybox;
private ParticleShaderProgram particleProgram;
private ParticleSystem particleSystem;
private ParticleShooter redParticleShooter;
private ParticleShooter greenParticleShooter;
private ParticleShooter blueParticleShooter;
private long globalStartTime;
private int particleTexture;
private int skyboxTexture;
private float xRotation, yRotation;
public ParticlesRenderer(Context context) {
this.context = context;
}
public void handleTouchDrag(float deltaX, float deltaY) {
xRotation += deltaX / 16f;
yRotation += deltaY / 16f;
if (yRotation < -90) {
yRotation = -90;
} else if (yRotation > 90) {
yRotation = 90;
}
// Setup view matrix
updateViewMatrices();
}
private void updateViewMatrices() {
setIdentityM(viewMatrix, 0);
rotateM(viewMatrix, 0, -yRotation, 1f, 0f, 0f);
rotateM(viewMatrix, 0, -xRotation, 0f, 1f, 0f);
System.arraycopy(viewMatrix, 0, viewMatrixForSkybox, 0, viewMatrix.length);
// We want the translation to apply to the regular view matrix, and not
// the skybox.
translateM(viewMatrix, 0, 0, -1.5f, -5f);
}
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
heightmapProgram = new HeightmapShaderProgram(context);
heightmap = new Heightmap(((BitmapDrawable)context.getResources()
.getDrawable(R.drawable.heightmap)).getBitmap());
skyboxProgram = new SkyboxShaderProgram(context);
skybox = new Skybox();
particleProgram = new ParticleShaderProgram(context);
particleSystem = new ParticleSystem(10000);
globalStartTime = System.nanoTime();
final Vector particleDirection = new Vector(0f, 0.5f, 0f);
final float angleVarianceInDegrees = 5f;
final float speedVariance = 1f;
redParticleShooter = new ParticleShooter(
new Point(-1f, 0f, 0f),
particleDirection,
Color.rgb(255, 50, 5),
angleVarianceInDegrees,
speedVariance);
greenParticleShooter = new ParticleShooter(
new Point(0f, 0f, 0f),
particleDirection,
Color.rgb(25, 255, 25),
angleVarianceInDegrees,
speedVariance);
blueParticleShooter = new ParticleShooter(
new Point(1f, 0f, 0f),
particleDirection,
Color.rgb(5, 50, 255),
angleVarianceInDegrees,
speedVariance);
particleTexture = TextureHelper.loadTexture(context, R.drawable.particle_texture);
skyboxTexture = TextureHelper.loadCubeMap(context,
new int[] { R.drawable.left, R.drawable.right,
R.drawable.bottom, R.drawable.top,
R.drawable.front, R.drawable.back});
}
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
glViewport(0, 0, width, height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width
/ (float) height, 1f, 100f);
/*
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width
/ (float) height, 1f, 10f);
*/
updateViewMatrices();
}
@Override
public void onDrawFrame(GL10 glUnused) {
/*
glClear(GL_COLOR_BUFFER_BIT);
*/
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawHeightmap();
drawSkybox();
drawParticles();
}
private void drawHeightmap() {
setIdentityM(modelMatrix, 0);
// Expand the heightmap's dimensions, but don't expand the height as
// much so that we don't get insanely tall mountains.
scaleM(modelMatrix, 0, 100f, 10f, 100f);
updateMvpMatrix();
heightmapProgram.useProgram();
heightmapProgram.setUniforms(modelViewProjectionMatrix);
heightmap.bindData(heightmapProgram);
heightmap.draw();
}
private void drawSkybox() {
setIdentityM(modelMatrix, 0);
updateMvpMatrixForSkybox();
glDepthFunc(GL_LEQUAL); // This avoids problems with the skybox itself getting clipped.
skyboxProgram.useProgram();
skyboxProgram.setUniforms(modelViewProjectionMatrix, skyboxTexture);
skybox.bindData(skyboxProgram);
skybox.draw();
glDepthFunc(GL_LESS);
}
private void drawParticles() {
float currentTime = (System.nanoTime() - globalStartTime) / 1000000000f;
redParticleShooter.addParticles(particleSystem, currentTime, 1);
greenParticleShooter.addParticles(particleSystem, currentTime, 1);
blueParticleShooter.addParticles(particleSystem, currentTime, 1);
setIdentityM(modelMatrix, 0);
updateMvpMatrix();
glDepthMask(false);
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
particleProgram.useProgram();
particleProgram.setUniforms(modelViewProjectionMatrix, currentTime, particleTexture);
particleSystem.bindData(particleProgram);
particleSystem.draw();
glDisable(GL_BLEND);
glDepthMask(true);
}
private void updateMvpMatrix() {
multiplyMM(tempMatrix, 0, viewMatrix, 0, modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
}
private void updateMvpMatrixForSkybox() {
multiplyMM(tempMatrix, 0, viewMatrixForSkybox, 0, modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
}
}
我們要繪製高度圖,就需要定義HeightmapShaderProgram着色器和Heightmap高度圖對象,開始的六個矩陣還是爲了進行運算定義的,只是爲了繪製天空盒,單獨把viewMatrixForSkybox抽出來了,如果大家對幾個矩陣運算的邏輯感覺頭暈,可以把幾個方法全部註釋掉,在每個對象的繪製方法中去進行矩陣運算也是可以的,這裏抽出來幾個方法完全是爲了複用;handleTouchDrag方法計算完角度,然後緊接着調用updateViewMatrices對矩陣進行處理;onSurfaceCreated方法中增加了glEnable(GL_DEPTH_TEST)、glEnable(GL_CULL_FACE)的邏輯,glEnable(GL_DEPTH_TEST)表示開啓深度測試功能,深度測試的意思是說,如果通過比較後深度值發生變化了,會進行更新深度緩衝區的操作。啓動它,OpenGL就會跟蹤再Z軸上的像素,這樣,它只會再那個像素前方沒有東西時,纔會繪畫這個像素;當前像素前面如果有別的像素擋住了它,那它就不會繪製,也就是說,OpenGL就只繪製最前面的一層。當我們需要繪製透明圖片時,就需要調用glDisable(GL_DEPTH_TEST)關閉它;glEnable(GL_CULL_FACE)表示剔除,上面我們已經講過了,就是按逆時針方向排布的捲曲三角形纔會被繪製,該句代碼是爲了節省性能的,我們如果把它註釋掉,可以看到效果沒有任何變化,因爲我們在前面定義三角形的頂點順序時,已經按逆時針的捲曲順序排布了。接着是構造heightmapProgram高度圖着色器程序和heightmap高度圖對象,高度圖對象傳入的Bitmap就是加載進來的的資源圖,有個疑問,爲什麼加載進來的資源圖是黑白色的,而最終繪製出來的高度圖卻是純綠色的?這是因爲在heightmap類的loadBitmapData方法中生成所有頂點數組時,我們只取了每個像素點的紅色分量來表示Y軸座標值,而且後面在頂點着色器中,大家還可以看到,我們把取到的Y值進行了插值轉換,相當於用紅色分量的值來表示高度,所以最終繪製出來的高度圖和加載的資源圖完全沒有一點關係了。
繼續看onSurfaceChanged方法中的邏輯,構造透視投影矩陣時,最後一個參數修改爲100f,表示Z軸繪製的範圍從-1到-100,如果我們把這個值修改的小一些,就可以看到,高度圖上原來比較遠、顏色淺的地方沒有了,因爲它的Z值超出了可視範圍被丟棄了,所以也不會被繪製;這裏調用updateViewMatrices是爲了初始化所有矩陣。
onDrawFrame方法中調用glClear方法清屏時,加了GL_DEPTH_BUFFER_BIT標誌位,因爲我們開啓的深度測試,所以需要使用glClear(GL_DEPTH_BUFFER_BIT),把所有像素的深度值設置爲最大值(一般是遠裁剪面),然後,在場景中以任意次序繪製所有物體。硬件或者軟件所執行的圖形計算把每一個繪製表面轉換爲窗口上一些像素的集合,此時並不考慮是否被其他物體遮擋。其次,OpenGL會計算這些表面和觀察平面的距離。如果啓用了深度緩衝區,在繪製每個像素之前,OpenGL會把它的深度值和已經存儲在這個像素的深度值進行比較。新像素深度值 < 原先像素深度值,也就是說新像素值更靠近前面,則新像素值會取代原先的;反之,新像素值被遮擋,他顏色值和深度將被丟棄。爲了啓動深度緩衝區,必須先啓動它,即glEnable(GL_DEPTH_TEST)。每次繪製場景之前,需要先清除深度緩衝區,即glClear(GL_DEPTH_BUFFER_BIT),然後以任意次序繪製場景中的物體。接下來封裝drawHeightmap()、drawSkybox()、drawParticles()三個方法分別繪製高度圖、天空盒、粒子系統,順序也是由遠到近的,封裝爲三個方法是爲了分別對每個對象進行操作,這樣就不會出現干擾。
最後,我們來看一下高度圖的頂點着色器和片頂着色器。頂點着色器的源碼如下:
uniform mat4 u_Matrix;
attribute vec3 a_Position;
varying vec3 v_Color;
void main()
{
v_Color = mix(vec3(0.180, 0.467, 0.153), // A dark green
vec3(0.660, 0.670, 0.680), // A stony gray
a_Position.y);
gl_Position = u_Matrix * vec4(a_Position, 1.0);
}
還是先定義矩陣變量u_Matrix、頂點屬性a_Position,它們的使用和粒子系統、天空盒完全一樣,我們就不講了;接着是要傳遞給片段着色的參數v_Color,main函數中使用mix函數實現以頂點數據的Y分量的線性混合,函數說明參考:wshxbqq/GLSL-Card,我們把對應的入參全部轉爲255表示的顏色值就是46、119、39和168、170、173,對應的顏色如下圖,也就是深綠色和灰色,這就是爲什麼我們看到的高度圖是綠色的原因了,當然,我們也可以使用紅色或者藍色作爲高度圖的背景色,只需要修改這裏的色值就可以了。
接着來看一下片段着色器,源碼如下:
precision mediump float;
varying vec3 v_Color;
void main()
{
gl_FragColor = vec4(v_Color, 1.0);
}
片段着色器的代碼非常簡單,就是使用我們線性插值的結果作爲片段的着色,賦值給內建變量gl_FragColor,相信如果大家讀懂了前幾節,對這段片段着色器的邏輯應該已經非常清楚了。
好了,我們本節的內容就分析完了,回頭想想,我們明白了實現一個結果當中所有的邏輯,但是要是真正拋開所有的代碼,讓我們自己從零開始完全一個這樣的需求,估計困難還是很大,非常多的地方都需要組織,說明我們當前還只是停留在表面,要達到精熟的目的,還有很長的一段路要走,這些還是需要通過實際的工作使用才能達到,讓我們繼續加油努力!!