OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
OpenGL之触控反馈
OpenGL之粒子
OpenGL之天空盒
高度图
高度图是表示高度的一个二维地图,很像地形图。一个创建高度图的简单方法是使用灰度图,亮的区域表示高地,而暗的区域表示低地,例如
顶点和索引缓冲区对象
为了加载高度图,要使用两个新的OpenGL对象:一个顶点缓冲区对象和一个索引缓冲区对象,类似于天空盒中所使用的顶点数组和索引数组,只是图形驱动器可以选择把这两个对象直接放进GPU的内存中。对于那些一经创建就不经常变化的对象来说,比如高度图,可以带来更好的性能
顶点缓冲区
public class VertexBuffer {
private static final String TAG = "VertexBuffer";
private int mBufferId;
/**
* 创建顶点数组
*
* @param data
*/
public VertexBuffer(float[] data) {
// 创建顶点缓冲区
int[] bufferId = new int[1];
GLES20.glGenBuffers(bufferId.length, bufferId, 0);
// 检查顶点缓冲区是否创建成功
if (bufferId[0] == 0) {
throw new RuntimeException("VertexBuffer: glGenBuffers failed!");
}
mBufferId = bufferId[0];
// 绑定顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, mBufferId);
// 将java数据复制到native层
FloatBuffer floatBuffer = ByteBuffer.allocateDirect(data.length * Constants.BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(data);
floatBuffer.position(0);
//将native层数据传递到GPU的buffer,GL_STATIC_DRAW:修改一次,经常使用
glBufferData(GL_ARRAY_BUFFER, floatBuffer.capacity() * Constants.BYTES_PER_FLOAT, floatBuffer, GL_STATIC_DRAW);
// 解绑顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
/**
* 绑顶点数据和OpenGL中的属性
*
* @param dataOffset 顶点数据偏移量,定位到数组中的那个位置
* @param attributeLocation 属性位置
* @param componentCount 此属性所占位数,例如位置(x,y)占两位,位置(x,y,z)占3位
* @param stride 所有属性占位总和字节数,例如(x,y,z,r,g,b)为6*4
*/
public void setVertexAttribPointer(int dataOffset, int attributeLocation, int componentCount, int stride) {
// 绑定顶点缓冲区
glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBufferId);
// 用了一个稍有不同的glVertexAttribPointer(),它的最后一个参数为int类型,而不是 Buffer对象
// 这个整型参数告诉OpenGL当前属性对应的以字节为单位的偏移值,对于第一个属性,它可能是0,对于其后续的属性,它就是一个指定的字节偏移值
glVertexAttribPointer(
attributeLocation,
componentCount,
GL_FLOAT,
false,
stride,
dataOffset
);
glEnableVertexAttribArray(attributeLocation);
// 解绑顶点缓冲区
glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
}
}
glBufferData的参数
int target:顶点缓冲区对象应该设GL_ARRAY_BUFFER,索引缓冲区对象应该设GL_ELEMENT_ARRAY_BUFFER
int size:数据的大小(以字节为单位)
Buffer data:由 allocateDirect() 创建的一个缓冲区 Buffer对象
int usage:告诉OpenGL对这个缓冲区对象所期望的使用模式
usage可用值 | 说明 |
---|---|
GL_STREAM_DRAW | 这个对象只会被修改一次,并且不会被经常使用 |
GL_STATIC_DRAW | 这个对象将被修改一次,但是会经常使用 |
GL_DYNAMIC_DRAW | 这个对象将被修改和使用很多次 |
这些只是提示,而不是限制,所以OpenGL可以根据需要做任何优化。大多数情况下,我们都使用GL_STATIC_DRAW
索引缓冲区
索引缓冲区和顶点缓冲区大致相似,只需要做以下修改
- 使用short[]和 ShortBuffer 作为类型
- 使用 GL_ELEMENT_ARRAY_BUFFER,而不是GL_ARRAY_BUFFER
- 要获得以字节为单位的大小,在Constants类中加入值为2的常量BYTES_PER_SHORT,并在你调用 glBufferData() 时使用那个常量,而不是BYTES_PER_FLOAT。当我们使用它绘制时,需要使用其缓冲区ID,因此需要对外提供索引缓冲区ID的方法
public class IndexBuffer {
private static final String TAG = "VertexBuffer";
private int mBufferId;
/**
* 创建顶点数组
*
* @param data
*/
public IndexBuffer(short[] data) {
int[] bufferId = new int[1];
GLES20.glGenBuffers(bufferId.length, bufferId, 0);
if (bufferId[0] == 0) {
throw new RuntimeException("IndexBuffer: glGenBuffers failed!");
}
mBufferId = bufferId[0];
ShortBuffer shortBuffer = ByteBuffer.allocateDirect(data.length * Constants.BYTES_PER_SHORT)
.order(ByteOrder.nativeOrder())
.asShortBuffer()
.put(data);
shortBuffer.position(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mBufferId);
//将本地数据传递到GPU的buffer,GL_STATIC_DRAW:修改一次,经常使用
glBufferData(GL_ELEMENT_ARRAY_BUFFER, shortBuffer.capacity() * Constants.BYTES_PER_SHORT, shortBuffer, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
public int getBufferId() {
return mBufferId;
}
}
加载高度图
要把高度图加载进OpenGL,需要加载图像数据,并把它转换为一组顶点,每个顶点对应一个像素,每个顶点都有一个基于其所在图像中的位置和一个基于像素亮度的高度,一旦所有的顶点都被加载进来,就可以使用索引缓冲区把它们组成OpenGL 能绘制的三角形
public class Heightmap {
private int mNumOfIndex;
private VertexBuffer mVertexBuffer;
private IndexBuffer mIndexBuffer;
private int mWidth;
private int mHeight;
public Heightmap(Bitmap bitmap) {
mWidth = bitmap.getWidth();
mHeight = bitmap.getHeight();
// 生成顶点数据
initVertexBuffer(bitmap);
// 生成索引数据
initIndexBuffer();
}
/**
* 绑定着色器中的 attribute 数据,告诉OpenGL当调用draw()时去顶点缓冲区获取数据
*
* @param heightmapProgram
*/
public void bindData(HeightmapProgram heightmapProgram) {
mVertexBuffer.setVertexAttribPointer(
0,
heightmapProgram.getAPosition(),
Constants.POSITION_COMPONENT_COUNT,
0
);
}
/**
* 绘制高度图
* 告诉OpenGL使用索引缓冲区绘制数据,最后一个参数使用了一个int类型的偏移,
* 而不是 Buffer对象的引用,用来告诉OpenGL从哪个索引开始读取数据
*/
public void draw() {
// 在使用之前绑定缓冲区
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer.getBufferId());
// 使用索引缓冲区绘制数据
glDrawElements(GL_TRIANGLES, mNumOfIndex, GL_UNSIGNED_SHORT, 0);
// 在使用之后解除绑定
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
/**
* 初始化索引Buffer
*/
private void initIndexBuffer() {
// 总共的索引数目
mNumOfIndex = calculateNumElements();
// 用于存储索引
short[] indexData = new short[mNumOfIndex];
// 索引偏移值
int offset = 0;
// 通过行和列的循环为每4个顶点构成的正方形创建三角形索引
for (short row = 0; row < mHeight - 1; row++) {
for (short col = 0; col < mWidth - 1; col++) {
// 构成正方形的四个顶点索引
short topLeft = (short) (row * mWidth + col);
short topRight = (short) (topLeft + 1);
short bottomLeft = (short) ((row + 1) * mWidth + col);
short bottomRight = (short) (bottomLeft + 1);
// 创建三角形索引
indexData[offset++] = topLeft;
indexData[offset++] = bottomLeft;
indexData[offset++] = topRight;
// 创建三角形索引
indexData[offset++] = topRight;
indexData[offset++] = bottomLeft;
indexData[offset++] = bottomRight;
}
}
mIndexBuffer = new IndexBuffer(indexData);
}
/**
* 初始化顶点Buffer
*
* @param bitmap
*/
private void initVertexBuffer(Bitmap bitmap) {
// 存储顶点数据
float[] vertexData = new float[mWidth * mHeight * Constants.POSITION_COMPONENT_COUNT];
// 顶点偏移值
int offset = 0;
// 存储bitmap的所有像素
int[] pixels = new int[mWidth * mHeight];
bitmap.getPixels(pixels, 0, mWidth, 0, 0, mWidth, mHeight);
// 遍历bitmap的所有像素,获取顶点数据
for (int row = 0; row < mHeight; row++) {
for (int col = 0; col < mWidth; col++) {
// 根据bitmap像素的位置和红色分量,得到顶点数据
float x = (col / (float) (mWidth - 1)) - 0.5f;
float y = Color.red(pixels[row * mHeight + col]) / 255f;
float z = (row / (float) (mHeight - 1)) - 0.5f;
// 将顶点数据写入顶点缓冲区
vertexData[offset++] = x;
vertexData[offset++] = y;
vertexData[offset++] = z;
}
}
mVertexBuffer = new VertexBuffer(vertexData);
}
/**
* 返回高度图的索引数目
* 相邻四个像素点形成1个正方形即2个三角形,即6个顶点,整个bitmap形成(mWidth - 1) * (mHeight - 1) 个正方形
*
* @return
*/
private int calculateNumElements() {
return (mWidth - 1) * (mHeight - 1) * 2 * 3;
}
}
绘制高度图
顶点着色器
uniform mat4 u_Matrix;
attribute vec3 a_Position;
varying vec3 v_Color;
void main()
{
v_Color = mix(vec3(0.180, 0.467, 0.153),
vec3(0.660, 0.670, 0.680),
a_Position.y);
gl_Position = u_Matrix * vec4(a_Position, 1.0);
}
顶点着色器使用了一个新的着色器函数 "mix()" 用来在两个不同的颜色间做平滑插值,我们配置了高度图,使其高度处于0和1之间,并使用这个高度作为两个颜色之间的比例,因此,高度图在接近底部的地方呈现绿色,在接近顶部的地方显示灰色。
片段着色器
precision mediump float;
varying vec3 v_Color;
void main()
{
gl_FragColor = vec4(v_Color, 1.0);
}
简单的将顶点着色器传过来平滑过的颜色赋值给最终片段颜色
封装高度图程序
public class HeightmapProgram extends BaseProgram {
private final int mUMatrix;
private final int mAPosition;
public HeightmapProgram(Context context) {
super(context, R.raw.particles8_heightmap_vertex, R.raw.particles8_heightmap_fragment);
mUMatrix = glGetUniformLocation(mProgram, U_MATRIX);
mAPosition = glGetAttribLocation(mProgram, A_POSITION);
}
public void setUniforms(float[] matrix) {
glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
}
public int getAPosition() {
return mAPosition;
}
}
在渲染器中加入高度图
深度缓冲区
用深度缓冲区可以消除隐藏面,OpenGL用深度缓冲区是一个特殊的缓冲区,用于记录屏幕上每个片段的深度。当这个缓冲区打开时,OpenGL会为每个片段执行深度测试算法:如果片段比已经存在的片段更近,就绘制它,否则,就丢掉它
打开深度缓冲区
在onSurfaceCreated内,在调用 glClearColor() 后面添加调用 glEnable(GL_DEPTH_TEST),打开深度缓冲区功能
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0f, 0f, 0f, 0f);
glEnable(GLES20.GL_DEPTH_TEST);
......
在 onDrawFrame() 中把 gIClear() 调用更新为 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT),这告诉OpenGL在每个新帧上也要清空深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
深度测试算法
由于之前配置了天空盒着色器程序,以使天空盒被绘制在远平面上。现在,深度测试功能被打开了,而OpenGL默认情况下只绘制那些比其他片段更近或者比远平面更近的片段,因此,看不见天空盒的部分了,如图
要修复这个问题,可以改变天空盒着色器,让它绘制得稍微近点,或者我们可以改变深度测试算法,让那些片段通过测试。编辑 drawSky(),按如下代码改变深度测试算法:
private void drawSky() {
// 深度测试算法改为小于等于,让天空盒被绘制出来
glDepthFunc(GL_LEQUAL);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrixForSky();
mSkyProgram.useProgram();
mSkyProgram.setUniforms(modelViewProjectionMatrix, mCubeTextureId);
mSkyBox.bindData(mSkyProgram);
mSkyBox.draw();
// 深度测试算法改回去,以免影响其他物体的绘制
glDepthFunc(GL_LESS);
}
默认情况下,深度测试被设置为使用GL_LESS,
GL_LESS:如果新片段比任何已经存在那里的片段近或者比远平面近,就让它通过测试
GL_LEQUAL:如果新片段与已经存在那里的片段相比较近或者二者在同等的距离处,就让它通过测试
绘制完天空盒,需要把深度测试重置为默认算法,使其他一切依然按预期的方式绘制出来
深度缓冲区和半透明物体
再次运行,可以看见天空盒了,但是,粒子现在被地面裁剪了,而且还彼此遮罩了
而我们希望粒子是半透明的、且相互混合,在粒子接触到地面的地方,我们需要―种方法使它们彼此不被阻挡,同时还要裁剪它们
在保持深度测试功能开启的同时禁用深度更新,可以实现这样的需求。这意味着粒子将针对地面进行测试,但是,其测试结果不会被写人深度缓冲区,这样它们就不会彼此阻挡了,因为我们最后才绘制粒子,因此这个方法行得通
使用 glDepthMask(false) 方法关闭深度测试的写操作
private void drawParticles() {
// 关闭深度测试的写操作
glDepthMask(false);
// 创建并添加粒子
float currentTime = (System.nanoTime() - mGlobalStartTime) / 1000000000f;
mRedParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mGreenParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mBlueParticleShooter.addParticle(mParticleSystem, currentTime, 5);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrix();
// 累计混合技术,粒子越多,就越亮
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
mParticlesProgram.useProgram();
// 设置uniform的值
mParticlesProgram.setUniforms(modelViewProjectionMatrix, currentTime, mTextureId);
// to do ,注释掉mParticleShooter.bindData(mParticlesProgram);模拟器就不会挂
// 绑定粒子系统数据
mParticleSystem.bindData(mParticlesProgram);
// 绘制粒子系统的所有粒子
mParticleSystem.draw();
// 一次绘制完成后,关闭累计混合技术
glDisable(GL_BLEND);
glDepthMask(true);
}
深度缓冲区和透视除法
深度缓冲区存储透视除法加工后的深度值,这在深度值和距离之间创建了一种非线性关系。它有这样一个效果,在与近平面接近的地方,深度的精度很高,而随着距离的增加,其精度也越来越低,这可能导致一些缺陷。由于这个原因,透视投影的近平面和远平面之间的比率不应该大于当前场景所需要的比率(也就是说,其近平面处的值为1,且其远平面处的值为100,平面可能没有问题,但是,其近平面处的值为0.001,而其远平面处的值为100 000.就会出现问题)
剔除
OpenGL为我们提供了另一种提高性能的方法,通过使能剔除(culling)技术消除隐藏面。默认情况下,OpenGL把默认所有的多边形表面当作两面渲染,可以告诉OpenGL关闭两面绘制,从而削减绘制开销
在onSurfaceCreated方法中,添加glEnable(GL_CULL_FACE),OpenGL就会查看每个三角形的卷曲顺序,它就是我们定义顶点的顺序,从观察点上看,如果这个卷曲顺序是逆时针的,这个三角形将被绘制出来,否则,它就会被丢掉
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0f, 0f, 0f, 0f);
glEnable(GLES20.GL_DEPTH_TEST);
glEnable(GLES20.GL_CULL_FACE);
渲染器代码
public class MyRenderer implements GLSurfaceView.Renderer {
private static final String TAG = "MyRenderer";
private static final int[] SKY_ID = new int[]{
R.drawable.left, R.drawable.right,
R.drawable.bottom, R.drawable.top,
R.drawable.front, R.drawable.back,
};
private Context mContext;
private float[] modelMatrix = new float[16];
private float[] viewMatrix = new float[16];
private float[] viewMatrixForSkybox = new float[16];
private float[] projectionMatrix = new float[16];
private float[] tempMatrix = new float[16];
private float[] modelViewProjectionMatrix = new float[16];
private HeightmapProgram mHeightmapProgram;
private Heightmap mHeightmap;
private ParticlesProgram mParticlesProgram;
private ParticleSystem mParticleSystem;
private ParticleShooter mRedParticleShooter;
private ParticleShooter mGreenParticleShooter;
private ParticleShooter mBlueParticleShooter;
// 控制粒子的角度
private float mAngleVarianceInDegree = 30f;
// 控制粒子的速度
private float mSpeedVariance = 1f;
private long mGlobalStartTime;
private SkyBox mSkyBox;
private SkyProgram mSkyProgram;
private int mTextureId;
private int mCubeTextureId;
private float xRotate;
private float yRotate;
public MyRenderer(Context context) {
mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
glClearColor(0f, 0f, 0f, 0f);
glEnable(GLES20.GL_DEPTH_TEST);
glEnable(GLES20.GL_CULL_FACE);
// 初始化高度图
mHeightmapProgram = new HeightmapProgram(mContext);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.heightmap, options);
mHeightmap = new Heightmap(bitmap);
// 粒子着色器程序
mParticlesProgram = new ParticlesProgram(mContext);
// 粒子系统,maxParticleCount如果太小的话,可能只能看到比较新的粒子
mParticleSystem = new ParticleSystem(10000);
// 粒子方向
Vector particleDirection = new Vector(0f, 0.5f, 0f);
// 粒子发射器
mRedParticleShooter = new ParticleShooter(new Point(-1f, 0f, 0f), particleDirection, Color.RED, mAngleVarianceInDegree, mSpeedVariance);
mGreenParticleShooter = new ParticleShooter(new Point(0f, 0f, 0f), particleDirection, Color.GREEN, mAngleVarianceInDegree, mSpeedVariance);
mBlueParticleShooter = new ParticleShooter(new Point(1f, 0f, 0f), particleDirection, Color.BLUE, mAngleVarianceInDegree, mSpeedVariance);
// 粒子系统启动的时间
mGlobalStartTime = System.nanoTime();
// 使用图片的样式绘制粒子
mTextureId = TextureHelper.loadTexture(mContext, R.drawable.particle_texture);
mSkyProgram = new SkyProgram(mContext);
mSkyBox = new SkyBox();
mCubeTextureId = TextureHelper.loadCubeTexture(mContext, SKY_ID);
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
glViewport(0, 0, width, height);
MatrixHelper.perspectiveM(projectionMatrix, 45, (float) width / height, 1, 10);
// 更新视图矩阵
updateViewMatrix();
}
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawSky();
drawHeightmap();
drawParticles();
}
private void drawHeightmap() {
// 更新矩阵
setIdentityM(modelMatrix, 0);
// 用模型矩阵使高度图在x和z方向上变宽100倍,而在y方向上只变高10倍
// 着色器中的颜色插值依赖于顶点所在位置的y值,这不会扰乱,因为在顶点着色器中,设置v_Color的时间是在我们把它与矩阵相乘之前
Matrix.scaleM(modelMatrix,0,100f,10f,100f);
updateMvpMatrix();
mHeightmapProgram.useProgram();
mHeightmapProgram.setUniforms(modelViewProjectionMatrix);
mHeightmap.bindData(mHeightmapProgram);
mHeightmap.draw();
}
private void drawParticles() {
// 关闭深度测试的写操作
glDepthMask(false);
// 创建并添加粒子
float currentTime = (System.nanoTime() - mGlobalStartTime) / 1000000000f;
mRedParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mGreenParticleShooter.addParticle(mParticleSystem, currentTime, 5);
mBlueParticleShooter.addParticle(mParticleSystem, currentTime, 5);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrix();
// 累计混合技术,粒子越多,就越亮
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE);
mParticlesProgram.useProgram();
// 设置uniform的值
mParticlesProgram.setUniforms(modelViewProjectionMatrix, currentTime, mTextureId);
// to do ,注释掉mParticleShooter.bindData(mParticlesProgram);模拟器就不会挂
// 绑定粒子系统数据
mParticleSystem.bindData(mParticlesProgram);
// 绘制粒子系统的所有粒子
mParticleSystem.draw();
// 一次绘制完成后,关闭累计混合技术
glDisable(GL_BLEND);
glDepthMask(true);
}
private void drawSky() {
// 深度测试算法改为小于等于,让天空盒被绘制出来
glDepthFunc(GL_LEQUAL);
// 更新矩阵
setIdentityM(modelMatrix, 0);
updateMvpMatrixForSky();
mSkyProgram.useProgram();
mSkyProgram.setUniforms(modelViewProjectionMatrix, mCubeTextureId);
mSkyBox.bindData(mSkyProgram);
mSkyBox.draw();
// 深度测试算法改回去,以免影响其他物体的绘制
glDepthFunc(GL_LESS);
}
/**
* 更新高度图和粒子的模型视图投影矩阵
*/
private void updateMvpMatrix() {
Matrix.multiplyMM(tempMatrix, 0, viewMatrix, 0, modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
}
/**
* 更新天空盒的模型视图投影矩阵
*/
private void updateMvpMatrixForSky() {
Matrix.multiplyMM(tempMatrix, 0, viewMatrixForSkybox, 0, modelMatrix, 0);
multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, tempMatrix, 0);
}
/**
* 处理拖拽事件
*
* @param deltaX
* @param deltaY
*/
public void handleDrag(float deltaX, float deltaY) {
Log.d(TAG, "handleDrag: deltaX=" + deltaX + " deltaY=" + deltaY);
xRotate += deltaX / 16f;
yRotate += deltaY / 16f;
if (yRotate > 90) {
yRotate = 90;
}
if (yRotate < -90) {
yRotate = -90;
}
// 更新视图矩阵
updateViewMatrix();
}
/**
* 更新视图矩阵,包括viewMatrix和viewMatrixForSkybox
* 高度图和粒子的视图矩阵viewMatrix,代表相机,它应用于所有的物体
* 天空盒视图矩阵viewMatrixForSkybox,只表示旋转
*/
private void updateViewMatrix() {
setIdentityM(viewMatrix, 0);
Matrix.rotateM(viewMatrix, 0, -yRotate, 1f, 0f, 0f);
Matrix.rotateM(viewMatrix, 0, -xRotate, 0f, 1f, 0f);
// 使用 viewMatrixForSkybox 旋转天空盒
System.arraycopy(viewMatrix, 0, viewMatrixForSkybox, 0, viewMatrix.length);
// 使用 viewMatrix 一起旋转和平移高度图和粒子
Matrix.translateM(viewMatrix, 0, 0, -1.5f, -5f);
}
}