<think>
我们已经在之前的回答中介绍了通过修改投影矩阵实现上下翻转的方法,这里将整合成一个完整的实现步骤,并提供必要的代码。
### 完整实现步骤:
1. **创建自定义渲染器**:继承 `GLSurfaceView.Renderer` 并实现三个方法:`onSurfaceCreated`, `onSurfaceChanged`, `onDrawFrame`。
2. **在 `onSurfaceChanged` 中设置翻转投影**:使用 `Matrix.orthoM` 创建正交投影矩阵,然后用 `Matrix.scaleM` 对 y 轴进行翻转(乘以-1)。
3. **设置着色器程序**:在 `onSurfaceCreated` 中编译着色器,并获取投影矩阵的 uniform 位置。
4. **在 `onDrawFrame` 中传递矩阵**:每次绘制时,将投影矩阵传递给着色器。
5. **处理顶点数据**:确保顶点坐标与翻转后的坐标系匹配(通常使用标准化的设备坐标,即NDC,范围为[-1,1])。
### 完整代码示例:
```java
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class FlipRenderer implements GLSurfaceView.Renderer {
private final float[] projectionMatrix = new float[16];
private int shaderProgram;
private int projectionMatrixHandle;
private int positionHandle;
// 顶点着色器代码
private final String vertexShaderCode =
"attribute vec4 vPosition;\n" +
"uniform mat4 u_ProjectionMatrix;\n" +
"void main() {\n" +
" gl_Position = u_ProjectionMatrix * vPosition;\n" +
"}";
// 片段着色器代码
private final String fragmentShaderCode =
"precision mediump float;\n" +
"uniform vec4 vColor;\n" +
"void main() {\n" +
" gl_FragColor = vColor;\n" +
"}";
// 顶点数据(一个三角形)
private final float[] triangleVertices = {
0.0f, 0.5f, 0.0f, // 顶点
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f // 右下
};
private final float[] color = {1.0f, 0.0f, 0.0f, 1.0f}; // 红色
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 设置背景色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 编译着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
// 创建着色器程序并链接
shaderProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(shaderProgram, vertexShader);
GLES20.glAttachShader(shaderProgram, fragmentShader);
GLES20.glLinkProgram(shaderProgram);
// 获取uniform和attribute的位置
projectionMatrixHandle = GLES20.glGetUniformLocation(shaderProgram, "u_ProjectionMatrix");
positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置视口
GLES20.glViewport(0, 0, width, height);
// 创建正交投影矩阵(初始:bottom=height, top=0 -> y向下)
Matrix.orthoM(projectionMatrix, 0, 0, width, height, 0, -1, 1);
// 翻转y轴:乘以-1,使得坐标系变成y向上(即上下翻转)
Matrix.scaleM(projectionMatrix, 0, 1, -1, 1);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 使用着色器程序
GLES20.glUseProgram(shaderProgram);
// 传递投影矩阵
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
// 启用顶点属性并传递顶点数据
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, BufferUtil.createFloatBuffer(triangleVertices));
// 设置颜色(这里简化处理,实际应该通过uniform传递)
int colorHandle = GLES20.glGetUniformLocation(shaderProgram, "vColor");
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(positionHandle);
}
// 辅助方法:编译着色器
private int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
}
// 工具类:用于创建FloatBuffer
class BufferUtil {
public static java.nio.FloatBuffer createFloatBuffer(float[] array) {
java.nio.FloatBuffer buffer = java.nio.ByteBuffer.allocateDirect(array.length * 4)
.order(java.nio.ByteOrder.nativeOrder())
.asFloatBuffer();
buffer.put(array).position(0);
return buffer;
}
}
```
### 使用说明:
1. 在Activity中设置GLSurfaceView使用此渲染器:
```java
GLSurfaceView glSurfaceView = findViewById(R.id.gl_surface_view);
glSurfaceView.setEGLContextClientVersion(2); // 使用OpenGL ES 2.0
glSurfaceView.setRenderer(new FlipRenderer());
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
```
2. **注意**:上述代码中的三角形顶点坐标是标准化设备坐标(NDC),范围[-1,1]。由于我们使用了正交投影,将屏幕坐标范围(0到width, 0到height)映射到NDC,并翻转y轴,因此原本在屏幕上方显示的三角形会翻转到下方。
### 翻转原理总结:
- 初始正交投影:`Matrix.orthoM(projectionMatrix, 0, 0, width, height, 0, -1, 1)` 将屏幕左下角设为(0,0),右上角设为(width, height),但此时y轴向下(因为bottom>top)。
- 通过`Matrix.scaleM(projectionMatrix, 0, 1, -1, 1)`将y轴缩放-1,实现上下翻转,使得坐标系变为y轴向上(即屏幕上方为y正方向)。
### 注意事项:
- 纹理坐标通常也需要翻转(如果不翻转,纹理会上下颠倒),可以通过在纹理坐标中翻转y(即用1减)或修改纹理矩阵实现。
- 触摸事件需要相应调整:屏幕触摸坐标系的y轴与翻转后的OpenGL坐标系y轴方向相反,需要转换。
§§相关问题§§
1. 在上下翻转后,如何正确加载纹理以避免纹理颠倒?
2. 除了正交投影,使用透视投影时如何实现上下翻转?
3. 如果想让GLSurfaceView同时支持上下翻转和左右翻转,应该如何修改?
4. 翻转坐标系后,如何处理用户触摸事件以匹配新的坐标系统?
5. 在OpenGL ES中,除了修改投影矩阵,还有哪些方法可以实现图像翻转?
</think>
以下是完整的 Android GLSurfaceView 显示上下翻转实现方案,包含所有必要组件和步骤:
### 完整实现代码(Java)
```java
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class FlipGLView extends GLSurfaceView {
public FlipGLView(Context context) {
super(context);
setEGLContextClientVersion(2); // 使用OpenGL ES 2.0
setRenderer(new FlipRenderer());
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
private static class FlipRenderer implements GLSurfaceView.Renderer {
// 投影矩阵
private final float[] projectionMatrix = new float[16];
// 着色器程序
private int shaderProgram;
private int positionHandle;
private int projectionMatrixHandle;
// 顶点数据 (显示一个三角形)
private final float[] triangleVertices = {
0.0f, 0.5f, 0.0f, // 顶点
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f // 右下
};
private FloatBuffer vertexBuffer;
// 顶点着色器源码
private final String vertexShaderCode =
"attribute vec4 vPosition;\n" +
"uniform mat4 u_ProjectionMatrix;\n" +
"void main() {\n" +
" gl_Position = u_ProjectionMatrix * vPosition;\n" +
"}";
// 片段着色器源码
private final String fragmentShaderCode =
"precision mediump float;\n" +
"void main() {\n" +
" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" + // 红色
"}";
public FlipRenderer() {
// 初始化顶点缓冲区
ByteBuffer bb = ByteBuffer.allocateDirect(triangleVertices.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(triangleVertices);
vertexBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 设置背景色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 编译着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
// 创建着色器程序
shaderProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(shaderProgram, vertexShader);
GLES20.glAttachShader(shaderProgram, fragmentShader);
GLES20.glLinkProgram(shaderProgram);
// 获取属性位置
positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
projectionMatrixHandle = GLES20.glGetUniformLocation(shaderProgram, "u_ProjectionMatrix");
}
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
// 设置视口
GLES20.glViewport(0, 0, width, height);
// 创建初始正交投影(坐标系:y轴向下)
Matrix.orthoM(projectionMatrix, 0, 0, width, height, 0, -1f, 1f);
// 关键步骤:翻转y轴(坐标系变为y轴向上)
Matrix.scaleM(projectionMatrix, 0, 1, -1, 1);
}
@Override
public void onDrawFrame(GL10 unused) {
// 清除屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 使用着色器程序
GLES20.glUseProgram(shaderProgram);
// 传递投影矩阵到着色器
GLES20.glUniformMatrix4fv(projectionMatrixHandle, 1, false, projectionMatrix, 0);
// 启用顶点属性
GLES20.glEnableVertexAttribArray(positionHandle);
// 准备顶点坐标数据
GLES20.glVertexAttribPointer(
positionHandle, // 属性位置索引
3, // 每个顶点的分量数 (x,y,z)
GLES20.GL_FLOAT, // 数据类型
false, // 是否归一化
0, // 步长
vertexBuffer // 顶点缓冲区
);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
// 禁用顶点属性
GLES20.glDisableVertexAttribArray(positionHandle);
}
private int loadShader(int type, String shaderCode) {
// 创建着色器
int shader = GLES20.glCreateShader(type);
// 添加源码并编译
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
}
}
```
### 使用说明(在Activity中调用)
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建GLSurfaceView
FlipGLView glView = new FlipGLView(this);
// 设置为Activity的内容视图
setContentView(glView);
}
}
```
### 关键实现原理
1. **投影矩阵翻转**:
```java
Matrix.orthoM(projectionMatrix, 0, 0, width, height, height, 0, -1f, 1f);
Matrix.scaleM(projectionMatrix, 0, 1, -1, 1); // y轴缩放-1实现翻转
```
2. **着色器处理**:
- 顶点着色器中使用投影矩阵变换坐标:
```glsl
gl_Position = u_ProjectionMatrix * vPosition;
```
3. **坐标系转换**:
- 翻转前:(0,0)在左上角,y轴向下正方向
- 翻转后:(0,0)在左下角,y轴向上正方向(标准OpenGL坐标系)
### 适配不同需求的调整
1. **纹理翻转处理**(如需显示图像):
```glsl
// 在片段着色器中使用纹理
"varying vec2 v_TexCoord;\n" +
"uniform sampler2D u_Texture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(u_Texture, vec2(v_TexCoord.x, 1.0 - v_TexCoord.y));\n" +
"}"
```
2. **触摸事件适配**:
```java
glView.setOnTouchListener((v, event) -> {
float y = getHeight() - event.getY(); // 翻转y坐标
// 处理翻转后的坐标...
return true;
});
```
### 常见问题解决方法
- **黑屏问题**:检查着色器编译日志 `GLES20.glGetShaderInfoLog(shader)`
- **几何体不显示**:确认顶点属性指针参数是否正确
- **翻转效果无效**:确认 `Matrix.scaleM` 在 `Matrix.orthoM` 之后调用