OpenGL之地形

OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
OpenGL之触控反馈
OpenGL之粒子
OpenGL之天空盒

高度图

高度图是表示高度的一个二维地图,很像地形图。一个创建高度图的简单方法是使用灰度图,亮的区域表示高地,而暗的区域表示低地,例如


heightmap.png

顶点和索引缓冲区对象

为了加载高度图,要使用两个新的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

索引缓冲区

索引缓冲区和顶点缓冲区大致相似,只需要做以下修改

  1. 使用short[]和 ShortBuffer 作为类型
  2. 使用 GL_ELEMENT_ARRAY_BUFFER,而不是GL_ARRAY_BUFFER
  3. 要获得以字节为单位的大小,在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默认情况下只绘制那些比其他片段更近或者比远平面更近的片段,因此,看不见天空盒的部分了,如图


image-20211223132855317.png

要修复这个问题,可以改变天空盒着色器,让它绘制得稍微近点,或者我们可以改变深度测试算法,让那些片段通过测试。编辑 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:如果新片段与已经存在那里的片段相比较近或者二者在同等的距离处,就让它通过测试

绘制完天空盒,需要把深度测试重置为默认算法,使其他一切依然按预期的方式绘制出来

深度缓冲区和半透明物体

再次运行,可以看见天空盒了,但是,粒子现在被地面裁剪了,而且还彼此遮罩了


image-20211223134141620.png

而我们希望粒子是半透明的、且相互混合,在粒子接触到地面的地方,我们需要―种方法使它们彼此不被阻挡,同时还要裁剪它们

在保持深度测试功能开启的同时禁用深度更新,可以实现这样的需求。这意味着粒子将针对地面进行测试,但是,其测试结果不会被写人深度缓冲区,这样它们就不会彼此阻挡了,因为我们最后才绘制粒子,因此这个方法行得通

使用 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);
    }

}

效果

高度图.gif
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容