Android显示系统(04)- OpenGL ES - Shader绘制三角形

一、前言:

OpenGL 1.0采用固定管线,OpenGL 2.0以上版本重要的改变就是采用了可编程管线,Shader 编程是指使用着色器(Shader)编写代码来控制图形渲染管线中特定阶段的处理过程。在图形渲染中,着色器是在 GPU 上执行的小型程序,用于定义图形渲染管线中不同阶段的处理逻辑,以实现各种视觉效果,那么渲染管线哪个阶段可编程呢?

就是上图的渲染管线蓝色部分。

二、Shader介绍:

1、Vertex Shader和Fragment Shader:

在现代的图形渲染中,通常会使用两种主要类型的着色器:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。这两种着色器分别负责处理图形的顶点数据和片元(像素)数据,通过编写这些着色器程序,开发人员可以实现各种复杂的图形效果和渲染技术。

下面是对顶点着色器和片元着色器的简要介绍:

  • 顶点着色器
    • 顶点着色器用于处理图形的顶点数据,如位置、颜色、法线等。
    • 主要作用包括对顶点位置的变换(如模型变换、视图变换、投影变换)、法线变换、顶点着色等。
    • 顶点着色器的输出通常是裁剪空间坐标或者屏幕空间坐标。
  • 片元着色器
    • 片元着色器用于处理图元的片元(像素)数据,负责计算最终的颜色输出。
    • 在片元着色器中,开发人员可以实现光照、纹理映射、阴影、透明度等效果。
    • 片元着色器的输出通常是片元的颜色、深度值、法线等。

2、图元:

在Shader编程中,"图元"(Primitive)指的是基本的几何图形单元,通常是指在3D图形渲染中的基本几何形状,如点、线、三角形、四边形等。这些基本的几何图形单元是构成复杂场景的基础,它们通过渲染管线进行处理和转换,最终呈现在屏幕上。

在Shader编程中,图元是渲染管线处理的基本单位,Shader程序会对每个图元进行相应的处理,包括顶点变换、光照计算、纹理映射等操作,最终将图元渲染到屏幕上。常见的几何图元有以下几种:

  1. 点(Point):最简单的图元,通常用于表示粒子、光源等。
  2. 线(Line):由两个点组成的图元,可用于绘制线条、边缘等。
  3. 三角形(Triangle):由三个顶点组成的图元,是最基本的多边形,是3D图形学中最重要的图元之一,因为所有复杂的表面都可以由三角形网格构成。
  4. 四边形(Quadrilateral):由四个顶点组成的图元,通常被拆分为两个三角形处理。

Shader程序通过对这些基本图元进行处理和变换,最终形成了复杂的场景和图像。在Shader编程中,开发人员可以通过编写顶点着色器和片元着色器来对这些图元进行处理,实现各种视觉效果和渲染技术。处理图元是Shader程序中的一个重要任务,它直接影响着最终的渲染效果和性能。

3、三角形:

上面的四个图元中,其实最重要的是三角形,我们把大多数复杂的图形都可以用三角形拼起来,就像我小时候(不敢说你们00后小时候)糊灯笼一样。

比如,我之前文章提到的这个复杂的图,也都是由众多小三角形构成:

三、重要坐标系:

在 OpenGL ES 中,"标准设备坐标系"(Normalized Device Coordinates)和"屏幕坐标系"(Screen Coordinates)是两种不同的坐标系,它们在图形渲染过程中扮演不同的角色。

  1. 标准设备坐标系(Normalized Device Coordinates)
    • 标准设备坐标系是一个抽象的坐标系,它是一个以屏幕空间的中心为原点,范围从 -1 到 1 的立方体空间。
    • 在标准设备坐标系中,坐标 (0, 0) 表示屏幕中心,(-1, -1) 表示左下角,(1, 1) 表示右上角。
    • 所有的顶点数据在通过VertexShader处理后都会被映射到标准设备坐标系,这是 OpenGL ES 中进行图形变换和裁剪的标准坐标系。
  2. 屏幕坐标系(Screen Coordinates)
    • 屏幕坐标系是实际显示设备的坐标系,它通常以左上角为原点,向右为 x 轴正方向,向下为 y 轴正方向。
    • 屏幕坐标系的坐标值通常是以像素为单位的整数值,用来确定在屏幕上绘制图像和文本等元素的位置。

在 OpenGL ES 渲染过程中,顶点数据首先被定义在对象坐标系中,然后通过模型变换、视图变换和投影变换将其转换到标准设备坐标系中,最终在屏幕上绘制出来。

总结来说,标准设备坐标系是为了方便进行图形变换和裁剪而定义的坐标系,而屏幕坐标系则是实际显示设备上的坐标系,用于确定最终图像的位置。OpenGL ES 中的渲染过程涉及将顶点数据从对象坐标系转换到标准设备坐标系,最终映射到屏幕坐标系进行显示。

四、GLSL语言:

1、概念:

着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

一个典型的着色器有下面的结构:

cpp 复制代码
#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

void main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

cpp 复制代码
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

通常情况下它至少会返回16个,大部分情况下是够用了。

2、数据传递:

  • 可以将Prog想象成一个芯片,有很多的引脚,比如这儿的1绑定的是VertextShader,2绑定的是FragmentShader。
  • 顶点着色器Vertex负责确定3D图形的三个顶点,片元着色器Fragment负责确定每个像素的颜色;
  • 数据传给1的时候,就会自动传给vPosition;看看具体怎么传递的:
    • 通过glGetAttribLocation获取vPositon的引脚;
    • glEnableVertextAttribArray传入的参数就是上一步获取的ID,这样,就可以打开这个引脚;
    • 通过glVertexAttribPointer将准备好的顶点数据Vertex Buffer Object传递给引脚1,这样vPosition就收到了;
  • 数据传给2的时候,就会自动传给vColor
    • 通过glGetUniformLocation获取引脚;
    • 通过glUniform4fv传递给vColor即可;

3、变量:

1)vertex shader变量:

  • 输入变量有:Attribute类型、Uniforms类型(unform变量相当于全局变量)、Samplers类型;
  • 输出变量有:Varying类型(Vertex Shader的输出,可以作为后续的Shader的输入)
  • 内部变量有:gl_Position(这个最重要);
    • 以gl开头的变量都是内部变量;
    • vertex shader需要给gl_Position赋值来确定顶点的位置;
    • 渲染管线会根据gl_Position进行图元装配;

2)fragment shader变量:

  • Varying类型:作为Fragment Shader的输入,和Vertex Shader的输出一一对应;
  • gl_FragColor:内部变量,是Fragment Shader的输出,保存每个像素的颜色;
    • 确定每个像素的最终颜色;
    • 可以和纹理结合,实现纹理的映射;
    • 还可以通过它实现光照、高亮等颜色特效;

五、绘制一个三角形:

1、步骤:

  • 使用GLSurfaceView创建OpenGL ES环境;

  • 定义顶点着色器和片元着色器以及OpenGL ES程序;

  • 编译顶点着色器和片元着色器;

    • 使用GLES30.glCreateShader()创建Shader对象;
    • 使用GLES30.glShaderSource()绑定Shader和其源代码;
    • 使用GLES30.glCompileShader()编译Shader;
  • 链接OpenGL ES程序;

    • 使用GLES30.glCreateProgram()创建OpenGL ES程序;
    • 使用GLES30.glAttachShader()绑定Shader到OpenGL ES程序;
    • 使用GLES30.glLinkProgram()链接整个OpenGL ES程序;
  • 使用OpenGL ES程序;

    • 调用GLES30.glUseProgram()使用OpenGL ES程序;
    • 传递顶点数据和片元数据

2、创建OpenGL ES环境:

定义GLSurfaceView

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/GLSurfaceViewTest.java
public class GLSurfaceViewTest extends GLSurfaceView {
    public GLSurfaceViewTest(Context context) {
        super(context);

        // 设置OpenGL ES版本(由于3.0兼容2.0,我们使用3.0)
        setEGLContextClientVersion(3);

        // 设置渲染器Renderer,函数调用后,里面会启动一个新线程构造EGL环境
        setRenderer(new GLRenderTest());
    }
}

MainActivity使用GLSurfaceView:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/MainActivity.java
public class MainActivity extends AppCompatActivity {
    private GLSurfaceViewTest mGlSurfaceViewTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGlSurfaceViewTest = new GLSurfaceViewTest(this);
        setContentView(mGlSurfaceViewTest);
    }
}

3、定义顶点着色器和片元着色器:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/Triangle.java
// 定义的顶点着色器代码
    private final String mVertexShaderCode =
            "attribute vec4 vPosition;" +
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "}";

    // 定义的片段着色器代码
    private final String mFragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    // 定义的三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
            0.0f, 0.2f, 0.0f,   // 顶部
            -0.5f, -0.5f, 0.0f, // 左下角
            0.5f, -0.5f, 0.0f   // 右下角
    };

    // 定义的fragment的颜色数组,表示每个像素的颜色
    private final float[] mColor = new float[]{0.0f, 1.0f, 0.0f, 1.0f};

4、定义OpenGL ES程序:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/Triangle.java
private int mProgram;
mProgram = GLES30.glCreateProgram();

5、编译着色器:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/Triangle.java
	public Triangle() {
        // ...

        // 2.加载并编译vertexShader和fragmentShader
        int vertexShader = Companion.compileShader(GLES30.GL_VERTEX_SHADER, mVertexShaderCode);
        int fragmentShader = Companion.compileShader(GLES30.GL_FRAGMENT_SHADER, mFragmentShaderCode);
       
       // ...
	}
    // 定义静态内部类
    public static class Companion {
        // 创建并编译着色器
        public static int compileShader(int type, String shaderCode) {
            // 创建一个着色器
            int shader = GLES30.glCreateShader(type);
            // 将着色器代码设置到着色器对象中
            GLES30.glShaderSource(shader, shaderCode);
            // 编译着色器
            GLES30.glCompileShader(shader);
            return shader;
        }
    }

6、链接OpenGL ES程序:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/Triangle.java
	public Triangle() {
        // ...
        // 4.attach两个编译好的着色器到program当中
        GLES30.glAttachShader(mProgram, vertexShader);
        GLES30.glAttachShader(mProgram, fragmentShader);
        // 5.链接整个program
        GLES30.glLinkProgram(mProgram);
    }

7、使用OpenGL ES程序:

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/Triangle.java
    public void draw() {
        // 使用program
        GLES30.glUseProgram(mProgram);
        // 获取顶点着色器的位置句柄
        mPositionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        // 启用顶点属性数组
        GLES30.glEnableVertexAttribArray(mPositionHandle);
        // 准备三角形坐标数据
        // 重置缓冲区位置
        mVertexBuffer.position(0);
        // 指定顶点属性数据的格式和位置
        GLES30.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES30.GL_FLOAT, false, 0, mVertexBuffer);

        // 获取片元着色器的颜色句柄
        mColorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
        // 设置绘制三角形的颜色
        GLES30.glUniform4fv(mColorHandle, 1, mColor, 0);
        // 绘制三角形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, mTriangleCoords.length / COORDS_PER_VERTEX);
        // 禁用顶点属性数组
        GLES30.glDisableVertexAttribArray(mPositionHandle);
    }

这就是一个绘制三角形的函数;

8、渲染器调用:

在渲染器中周期性地调用上面的绘制函数。

java 复制代码
// 文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java
public class GLRenderTest implements GLSurfaceView.Renderer {
    private Triangle mTriangle;
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        mTriangle = new Triangle();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0,  width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl){
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }
}

9、运行结果:

10、附:全部代码:

文件路径:com/example/glsurfaceviewdemo/MainActivity.java

java 复制代码
package com.example.glsurfaceviewdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    private GLSurfaceViewTest mGlSurfaceViewTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGlSurfaceViewTest = new GLSurfaceViewTest(this);
        setContentView(mGlSurfaceViewTest);
    }
}

文件路径:com/example/glsurfaceviewdemo/GLSurfaceViewTest.java

java 复制代码
package com.example.glsurfaceviewdemo;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;

public class GLSurfaceViewTest extends GLSurfaceView {
    public GLSurfaceViewTest(Context context) {
        super(context);

        // 设置OpenGL ES版本(由于3.0兼容2.0,我们使用3.0)
        setEGLContextClientVersion(3);

        // 设置渲染器Renderer,函数调用后,里面会启动一个新线程构造EGL环境
        setRenderer(new GLRenderTest());
    }
}

文件路径:com/example/glsurfaceviewdemo/GLRenderTest.java

java 复制代码
package com.example.glsurfaceviewdemo;

import android.opengl.GLES30;
import android.opengl.GLSurfaceView;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class GLRenderTest implements GLSurfaceView.Renderer {
    private Triangle mTriangle;
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES30.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        mTriangle = new Triangle();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0,  width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl){
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
        mTriangle.draw();
    }
}

文件路径:com/example/glsurfaceviewdemo/Triangle.java

java 复制代码
package com.example.glsurfaceviewdemo;

import android.opengl.GLES30;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import javax.microedition.khronos.opengles.GL;

public class Triangle {
    public Triangle() {
        // 1.初始化顶点缓冲区,存储三角形坐标
        // 为顶点坐标分配DMA内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(mTriangleCoords.length * 4);
        // 设置字节顺序为本地字节顺序(会根据硬件架构自适应大小端)
        byteBuffer.order(ByteOrder.nativeOrder());
        // 将字节缓冲区转换为浮点缓冲区
        mVertexBuffer = byteBuffer.asFloatBuffer();
        // 将顶点三角形坐标放入缓冲区
        mVertexBuffer.put(mTriangleCoords);
        // 设置缓冲区的位置指针到起始位置
        mVertexBuffer.position(0);

        // 2.加载并编译vertexShader和fragmentShader
        int vertexShader = Companion.compileShader(GLES30.GL_VERTEX_SHADER, mVertexShaderCode);
        int fragmentShader = Companion.compileShader(GLES30.GL_FRAGMENT_SHADER, mFragmentShaderCode);
        // 3.创建一个OpenGL程序
        mProgram = GLES30.glCreateProgram();
        // 4.attach两个编译好的着色器到program当中
        GLES30.glAttachShader(mProgram, vertexShader);
        GLES30.glAttachShader(mProgram, fragmentShader);
        // 5.链接整个program
        GLES30.glLinkProgram(mProgram);
    }

    // 顶点数据是float类型,因此,使用这个存储
    private FloatBuffer mVertexBuffer;
    private int mProgram;
    // 定义的顶点着色器代码
    private final String mVertexShaderCode =
            "attribute vec4 vPosition;" +
                    "void main() {" +
                    "  gl_Position = vPosition;" +
                    "}";

    // 定义的片段着色器代码
    private final String mFragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    // 定义的三角形顶点坐标数组
    private final float[] mTriangleCoords = new float[]{
            0.0f, 0.2f, 0.0f,   // 顶部
            -0.5f, -0.5f, 0.0f, // 左下角
            0.5f, -0.5f, 0.0f   // 右下角
    };

    // 定义的fragment的颜色数组,表示每个像素的颜色
    private final float[] mColor = new float[]{0.0f, 1.0f, 0.0f, 1.0f};
    // 顶点着色器的位置句柄
    private int mPositionHandle = 0;
    // 片元着色器的位置句柄
    private int mColorHandle = 0;
    private final int COORDS_PER_VERTEX = 3;

    // 定义静态内部类
    public static class Companion {
        // 创建并编译着色器
        public static int compileShader(int type, String shaderCode) {
            // 创建一个着色器
            int shader = GLES30.glCreateShader(type);
            // 将着色器代码设置到着色器对象中
            GLES30.glShaderSource(shader, shaderCode);
            // 编译着色器
            GLES30.glCompileShader(shader);
            return shader;
        }
    }

    public void draw() {
        // 使用program
        GLES30.glUseProgram(mProgram);
        // 获取顶点着色器的位置句柄
        mPositionHandle = GLES30.glGetAttribLocation(mProgram, "vPosition");
        // 启用顶点属性数组
        GLES30.glEnableVertexAttribArray(mPositionHandle);
        // 准备三角形坐标数据
        // 重置缓冲区位置
        mVertexBuffer.position(0);
        // 指定顶点属性数据的格式和位置
        GLES30.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES30.GL_FLOAT, false, 0, mVertexBuffer);

        // 获取片元着色器的颜色句柄
        mColorHandle = GLES30.glGetUniformLocation(mProgram, "vColor");
        // 设置绘制三角形的颜色
        GLES30.glUniform4fv(mColorHandle, 1, mColor, 0);
        // 绘制三角形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, mTriangleCoords.length / COORDS_PER_VERTEX);
        // 禁用顶点属性数组
        GLES30.glDisableVertexAttribArray(mPositionHandle);
    }
}

六、总结:

本文主要介绍了Shader以及GLSL语言,同时画出了一个三角形,但是一般工程中GLSL语言会用单独的文件写,后续我们改进下!

相关推荐
孑么7 小时前
GDPU Android移动应用 重点习题集
android·xml·java·okhttp·kotlin·android studio·webview
@OuYang9 小时前
Audio音频输出通道
android
Eric.Lee202110 小时前
不同方式获取音频时长 - python 实现
音视频·音频处理·音频时长·音频时长限制
爱研究的小牛10 小时前
Synthesia技术浅析(二):虚拟人物视频生成
人工智能·深度学习·机器学习·aigc·音视频
ta叫我小白10 小时前
Kotlin 中 forEach 的 return@forEach 的使用误区
android·开发语言·kotlin
archko10 小时前
试用kotlin multiplatform
android·开发语言·kotlin
C4rpeDime11 小时前
当歌 - RSS 订阅分发平台开发
android
cuijiecheng201812 小时前
音视频入门基础:MPEG2-PS专题(5)——FFmpeg源码中,解析PS流中的PES流的实现
ffmpeg·音视频
大雄野比13 小时前
UOS系统mysql服务安装
android·mysql·adb
yozyyyqls13 小时前
自定义Compose Pager实现电影卡片列表
android