OpenGL 入门(一)—— OpenGL 基础

1、OpenGL 基础知识

1.1 OpenGL 简介

OpenGL(Open Graphics Library)是图形领域的工业标准,是一套跨编程语言、跨平台、专业的图形编程(软件)接口。它用于二维、三维图像,是一个功能强大,调用方便的底层图形库。它与硬件无关,可以在不同的平台如 Windows、Linux、Mac、Android、IOS 之间进行移植。因此,支持 OpenGL 的软件具有很好的移植性,可以获得非常广泛的应用(比如 PS 在部分功能和操作中使用 OpenGL 加速,以提高图像处理和渲染的性能)。

对于 Android 系统而言,它支持的是 OpenGL ES。OpenGL ES 是针对手机、PDA 和游戏主机等嵌入式设备设计的 OpenGL API 的子集。下面是 Android 系统版本与 OpenGL ES 的支持版本的一般对照表(具体的设备和制造商可能会在特定型号上进行适配和调整):

  • Android 4.0(Ice Cream Sandwich):OpenGL ES 2.0
  • Android 4.1 - 4.3(Jelly Bean):OpenGL ES 2.0/3.0
  • Android 4.4(KitKat):OpenGL ES 2.0/3.0/3.1
  • Android 5.0 - 5.1(Lollipop):OpenGL ES 3.1/3.2
  • Android 6.0(Marshmallow):OpenGL ES 3.1/3.2
  • Android 7.0 - 12.0:OpenGL ES 3.2

OpenGL 官方上提供了很多资料,作为 Android 开发者需要关注 OpenSL ES,可以在OpenGL 官网页面找到两个 OpenSL ES 的资料:

请添加图片描述

第一个是 OpenGL ES 的官方文档:

2024-3-18.OpenGL ES官方文档0

可以在右侧选择版本:

2024-3-18.OpenGL ES官方文档

第二个是 OpenGL ES 的 API 快速查找手册:

在这里插入图片描述

在这里插入图片描述

1.2 OpenGL 绘制原理

OpenGL 是可以绘制 3D 图像的,比如绘制一头牛的过程如下:

2024-3-18.OpenGL绘制流程

  1. Vertex Processing:首先将牛抽象为 3D 网格,在网格中牛由无数个像素点组成,将这些像素点视为顶点,每 3 个顶点可以构成一个三角形,这个三角形可以视为一个纹理,多个纹理可以组成一个面,最终形成一头牛。其中,将 3D 网格转化为顶点的过程是由顶点着色器完成的
  2. Rasterization:光栅化,将几何图形转换为在屏幕上显示的像素网格得到片元
  3. Raster Operations:进行纹理过滤,使用片元着色器对片元上色
  4. Fragment Processing:经过片元处理得到最终要输出的图像

以上过程涉及到一些 OpenGL 的专业名词,解释如下:

  1. 纹理(Texture):是将图像或图案应用到几何图形上的一种技术。
    • 纹理可以理解为在三维空间中的一个图像或图案,它可以覆盖在三维对象的表面上,给出了表面的颜色和细节。实际上是通过将二维的纹理映射到三维的几何图形的表面上,从而实现对几何图形进行贴图,使其呈现出真实的外观和细节。
    • 纹理通常是由像素组成的二维图像,可以是从图像文件加载的位图,或者是通过代码生成的程序纹理。在 OpenGL 中,纹理坐标(Texture Coordinates)被用来映射纹理到三维几何图形上的对应位置。
    • 纹理可以看作是一个二维数组,由一系列像素组成。每个像素包含了图像的颜色信息或其他数据。纹理可以包含不同的图像格式,例如 RGB、RGBA、灰度图等。
  2. 顶点着色器(Vertex Shader):顶点着色器是一种在图形渲染管线中执行的程序,用于处理输入的顶点数据。它以顶点为基本单位,对每个输入顶点进行处理和变换,例如对顶点位置、颜色、法线等进行变换、计算和插值。顶点着色器通常用于执行顶点变换、几何操作和顶点属性的计算。
  3. 光栅化(Rasterization):是计算机图形学中的一个重要概念,用于将几何图形转换为在屏幕上显示的像素图像。光栅化的过程涉及将连续的几何图形转换为离散的像素表示。在三维图形渲染中,几何图形通常是由三角形组成的三角网格(Triangle Mesh)。光栅化将每个三角形映射到屏幕上的像素,并确定每个像素的颜色和深度值。其主要步骤包括:
    • 顶点处理(Vertex Processing):在光栅化之前,对每个顶点进行变换、投影和其他处理,以将顶点从三维空间转换到屏幕空间。
    • 三角形设置(Triangle Setup):确定每个三角形的边界框(bounding box)或边界区域,以限定光栅化的范围。
    • 逐像素处理(Pixel Processing):对于每个位于三角形边界框内的像素,确定其是否位于三角形内部,并计算其颜色、纹理坐标、深度值等属性。
    • 插值计算(Interpolation):根据顶点属性的值,通过插值计算来获取每个像素的准确属性值,如颜色插值、纹理坐标插值等。
    • 像素输出(Pixel Output):根据计算得到的像素属性值,将其写入帧缓冲区(Frame Buffer),最终形成屏幕上的图像
  4. 片元着色器(Fragment Shader):片元着色器是一种在图形渲染管线中执行的程序,用于处理光栅化后的片元(像素)数据。它以片元为基本单位,对每个片元进行处理和计算,例如计算片元的颜色、法线、光照等。片元着色器通常用于执行光照模型、纹理采样、混合和其他片元级别的计算。

顶点着色器和片元着色器需要程序员自行实现。

1.3 Android 绘制原理

Android 在绘制时实际上就用到了 OpenGL。比如绘制一个 Button:

3.3.1-Android绘制原理

大致步骤如下:

  • LayoutInflater 解析布局文件,将 Button 从 xml 的标签解析成包含边界和宽高等信息的 Button 对象
  • CPU 对 Button 对象内的边界、宽高、颜色等进行计算,处理成纹理(多维的向量图形)交给 GPU
  • GPU 负责填充,将向量图形栅格化(转换成像素位图图像)再画到屏幕上

实际上 OpenGL 就是工作在 GPU 当中的,下图从 CPU、GPU 和屏幕的维度描绘了绘制过程:

3.3.1-Android渲染机制分析.PNG

既然提到 CPU 与 GPU,就要简单看一下二者的区别与联系:

3.3.1-CPU与GPU的区别1

3.3.1-CPU与GPU的区别2

1.4 OpenGL 坐标系

OpenGL 编程会常用到如下三种坐标系:

2024-3-18.OpenGL世界坐标系OpenGL世界坐标系的边界为1
Android坐标系原点是屏幕左上角
纹理坐标系原点是左下角
  • OpenGL 世界坐标系的原点 (0,0) 在中心,边界最大坐标为 1
  • Android 屏幕坐标系的原点 (0,0) 在左上角,边界也是 1
  • OpenGL 纹理坐标系的原点 (0,0) 在左下角,边界还是 1

OpenGL 世界坐标系与 Android 屏幕坐标系的顶点是有对应关系的,如图所示:

2024-4-8.OpenGL坐标系对应关系

2、OpenGL SL 语法基础

OpenGL 编程使用的着色器语言。

着色器语言(Shader Language)是一种特定于图形编程的编程语言,用于编写顶点着色器、片元着色器和其他类型的着色器程序。在 OpenGL 中,常用的着色器语言是 GLSL(OpenGL Shading Language),它是一种高级着色器语言,用于描述和执行图形渲染管线中的着色器程序。GLSL 提供了丰富的语法和内置函数,使得开发者可以编写灵活和高效的着色器代码。

以下是后续 Demo 中常用的语法内容:

数据类型描述
float浮点型
vec2含两个浮点型数据的向量
vec4含四个浮点型数据的向量(xyzw、rgba、stpq)
sampler2D2D纹理采样器(代表一层纹理,用uniform来修饰)
修饰符描述
attribute属性变量。只能用于顶点着色器中。一般用该变量来表示一些顶点数据,如:顶点坐标、纹理坐标、颜色等。
uniform一致变量。在着色器执行期间一致变量的值是不变的。与 const 常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。
varying易变变量。是从顶点着色器传递到片元着色器的数据变量。
内建函数描述
texture2D(采样器, 坐标) 采样指定位置的纹理

这里顺便了解一下 OpenGL 函数的命名格式:

<函数库前缀> <根命令> <可选的参数数量> <可选的参数类型>

2024-3-18.OpenGL命名规则

使用参数数量和参数类型是因为面向过程的 C 语言中没有函数重载。

内建变量描述
gl_Positionvec4类型,表示顶点着色器中顶点位置
gl_FragColorvec4类型,表示片元着色器中颜色
精度名称描述
precision lowp低精度
precision mediump中精度
precision highp高精度

举个示例,比如要声明一个 4 * 4 的矩阵:

mat4 m1 = mat4(1.0, 2.0, 3.0, 4.0,1.0, 2.0, 3.0, 4.0,1.0, 2.0, 3.0, 4.0,1.0, 2.0, 3.0, 4.0
)mat4 m2 = mat4(2.0)mat4 m3 = mat4(2.0, 0.0, 0.0, 0.0,0.0, 2.0, 0.0, 0.0,0.0, 0.0, 2.0, 0.0,0.0, 0.0, 0.0, 2.0
)

其中 m2 初始化的结果与 m3 相同,即初始化左上角到右下角对角线上位置上的数据,其余位置均为 0。

3、OpenGL SL 简单使用

这一节我们使用 OpenGL SL 在 Android 屏幕上画一个等腰直角三角形。

3.1 OpenGL SL 简介

首先我们要清楚,OpenGL SL 本质上是一个 NDK 的动态库,在 NDK 的 toolchains 中可以找到它们:

2024-3-26.OpenGL Native库1

但是 Google 将 NDK 接口封装为 Java API 方便上层调用:

2024-3-26.OpenGL上层封装

图形渲染管线

OpenGL 图形渲染管线(计算机图形学中用于将三维模型转换为最终渲染图像的流程和算法)的简化流程图,它展示了顶点着色器和片元着色器之间的数据流:

2024-3-18.GLSL

OpenGL 绘制时需要自行实现顶点着色器和片元着色器:

  1. 顶点着色器的输入数据包括顶点属性(如位置、法线等)以及可选的采样器。输出数据是变换后的顶点位置(gl_Position)、正面朝向标志(gl_FrontFacing)以及其他临时变量
    • 首先,顶点着色器接收来自应用程序的顶点数据,这些数据通常包含顶点的位置、法线、纹理坐标等信息。顶点着色器的主要任务是对这些顶点进行变换,例如应用模型矩阵、视图矩阵和投影矩阵,以将顶点从对象空间转换到屏幕空间
    • 然后,顶点着色器会生成一个 gl_Position 变量,它是经过变换后的顶点位置,表示该顶点在屏幕上的位置。此外,顶点着色器还可以生成其他临时变量,用于后续的计算或传递给片元着色器
    • 接下来,顶点着色器会将变换后的顶点数据传递给片元着色器。在这个过程中,顶点着色器还会生成一些 Varying 变量,这些变量可以用来存储顶点之间的一些信息,比如纹理坐标或者光照参数
  2. 片元着色器输入数据包括从顶点着色器传递过来的 Varying 变量(如纹理坐标),以及一些内置变量(如点大小 gl_PointSize 和片段坐标 gl_FragCoord)。输出数据是一个颜色值(gl_FragColor),它将被用于最终的图像渲染
    • 片元着色器接收到顶点着色器传递来的 Varying 变量,以及一些内置变量,如 gl_FragCoord 和 gl_PointSize 等。片元着色器的主要任务是对每个像素的颜色进行计算,这可能涉及到纹理采样、光照计算、混合操作等
    • 最后,片元着色器会生成一个 gl_FragColor 变量,这是最终的颜色值,它会被用于绘制像素。如果需要,片元着色器也可以生成其他临时变量,用于后续的计算或传递给其他阶段

可编程管线

再来看 OpenGL 2.0 增加的可编程管线,它显示了图形渲染管线的基本工作流程:

2024-3-18.可编程管线

各部分职能:

  • 顶点着色器:是图形渲染管线的第一步,它主要负责处理顶点数据,包括顶点位置、法线、纹理坐标等。顶点着色器可以根据需要修改顶点数据,例如应用变换矩阵、添加顶点动画效果等
  • 图元装配:将顶点数据组装成图元的过程,常见的图元有三角形、矩形等。图元装配的结果是一组有序的顶点列表,这些顶点将被发送到光栅化阶段
  • 光栅化:将图元转换为像素的过程,它根据图元的几何形状和屏幕分辨率,确定哪些像素应该被填充。光栅化阶段还负责执行裁剪、深度测试、模板测试等操作
  • 片元着色器:是图形渲染管线中的一个重要步骤,它负责处理每个像素的颜色信息。片元着色器可以从顶点着色器获取数据,例如顶点位置、法线、纹理坐标等,然后根据这些数据计算出每个像素的颜色
  • 逐片元处理:在片元着色器之后的一个阶段,它负责执行各种后期处理效果,例如模糊、抗锯齿、景深等。这些效果可以在片元着色器之后应用,从而实现更复杂的视觉效果
  • 帧缓冲区:是图形渲染管线的最后一个阶段,它负责将渲染结果保存到屏幕上。帧缓冲区可以是硬件设备的一部分,也可以是软件模拟的内存区域

顶点着色器只是确定了图形的外部边界轮廓,而片元着色器才负责绘制内部的纹理(纹路)与颜色。

主要组件

最后来了解 Android OpenGL ES 包含的主要组件:

  • GLSurfaceView:继承自 SurfaceView,其内嵌的 Surface 专门负责 OpenGL 渲染。它支持:
    • 管理 Surface 与 EGL
    • 允许自定义渲染器 Render
    • 让渲染器在独立的线程里运作,和 UI 线程分离
    • 按需渲染(on-demand)和连续渲染(continuous)
  • EGL:OpenGL 是一个跨平台操作 GPU 的 API,但 OpenGL 需要与本地视窗系统进行交互,这就需要一个中间控制层,EGL 就是链接 OpenGL ES 和本地窗口系统的接口,引入 EGL 就是为了屏蔽不同平台上的区别(类似于上层通过 JNI 实现与本地代码的交互)

知道大致流程后可以进入代码阶段了。

3.2 初始化

首先在 AndroidManifest 中声明使用 OpenGL:

<uses-feature android:glEsVersion="0x00020000" android:required="true"/>

然后声明一个自定义 View 继承 GLSurfaceView,并设置 EGL 版本、渲染器和渲染模式:

// GLSurfaceView 继承了 SurfaceView,它自带一个线程 GLThread 进行渲染工作
class GLView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs) {constructor(context: Context?) : this(context, null)init {// 设置 EGL 版本setEGLContextClientVersion(2)// 设置渲染器setRenderer(GLRender())// 设置渲染模式:// RENDERMODE_WHEN_DIRTY 会在 Surface 被创建,或调用 requestRender() 时渲染// RENDERMODE_CONTINUOUSLY 会每隔一段时间自动渲染renderMode = RENDERMODE_WHEN_DIRTY}
}

再声明自定义渲染器 GLRender 继承 GLSurfaceView.Render:

class GLRender : GLSurfaceView.Renderer {// GLSurfaceView.Renderer startoverride fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {// 将画布清空为黑色GLES20.glClearColor(0f, 0f, 0f, 0f)}override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {}override fun onDrawFrame(gl: GL10?) {}// GLSurfaceView.Renderer end
}

3.3 初始化三角形

我们新建一个 Triangle 来处理三角形的绘制,并且在 GLRender 绘画回调时,将回调同步给 Triangle:

class GLRender : GLSurfaceView.Renderer {override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {// 将画布清空为黑色GLES20.glClearColor(0f, 0f, 0f, 0f)triangle = Triangle()}// 当外界调用 requestRender 时会触发本方法回调,// 类似于 invalidate 与 onDraw 的关系override fun onDrawFrame(gl: GL10?) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT)// 具体的渲染工作交给 Triangle 对象triangle.onDrawFrame(gl)}
}

接下来要确定被绘制的三角形是什么样的。比如我想画一个在 z = 0 这个平面的等腰直角三角形,如下图所示:

2024-3-17.OpenGL坐标系

那么该三角形的顶点数组声明如下:

	companion object {val triangleCoordinates = floatArrayOf(0.5f, 0.5f, 0.0f,-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f)}

接下来需要创建顶点着色器和片元着色器了。首先我们在 AS 中安装 GLSL Support 插件,可以对 GLSL 进行高亮提示。然后在 /res/raw/ 目录下新建顶点着色器文件 vertex_shader.glsl:

// attribute 声明了包含四个浮点数的向量 vPosition
attribute vec4 vPosition;
// main 是着色器程序的入口点
void main() {// gl_Position 是语言内置变量,代表了顶点的位置gl_Position=vPosition;
}

然后是片元着色器 fragment_shader.glsl:

precision mediump float;
uniform vec4 vColor;
void main() {// 将颜色向量赋值给内置变量,OpenGL 就会根据该颜色绘制gl_FragColor=vColor;
}

接下来你可以将两个着色器的代码声明为字符串变量,或者通过 IO 流读取文件代码到字符串。这里为了方便我们采用第一种方式(后续 Demo 会采用第二种方式):

	private val vertexShaderCode = "attribute vec4 vPosition;\n" +"void main() {\n" +"    gl_Position=vPosition;\n" +"}"private val fragmentShaderCode = "precision mediump float;\n" +"uniform vec4 vColor;\n" +"void main() {\n" +"    gl_FragColor=vColor;\n" +"}"

最后在 Triangle 的构造方法中做如下的初始化工作:

	private var vertexBuffer: FloatBufferprivate var program = 0init {// 在 GPU 中申请内存,三角形有 3 个顶点,每个顶点有 3 个维度,都是 4 个字节的 float 类型val byteBuffer = ByteBuffer.allocateDirect(triangleCoordinates.size * 4)// ByteBuffer 内元素排列顺序采用 Native 顺序byteBuffer.order(ByteOrder.nativeOrder())// GL 语言代码需要通过 vertexBuffer 传入 GPUvertexBuffer = byteBuffer.asFloatBuffer()vertexBuffer.put(triangleCoordinates)vertexBuffer.position(0)// 创建顶点着色器并在 GPU 中进行编译val vertexShader: Int = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)GLES20.glShaderSource(vertexShader, vertexShaderCode)GLES20.glCompileShader(vertexShader)// 创建片元着色器并在 GPU 中进行编译val fragmentShader: Int = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER)GLES20.glShaderSource(fragmentShader, fragmentShaderCode)GLES20.glCompileShader(fragmentShader)// 创建 Program 并绑定两个着色器program = GLES20.glCreateProgram()GLES20.glAttachShader(program, vertexShader)GLES20.glAttachShader(program, fragmentShader)// 连接到着色器程序GLES20.glLinkProgram(program)}

3.4 绘制三角形

最后在 GLRender 传入的绘制回调方法 onDrawFrame() 中绘制三角形:

	private val colors = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f)fun onDrawFrame(gl: GL10?) {// 渲染GLES20.glUseProgram(program)// 获取 GPU 中 vPosition 变量的地址(实际上是一个 Native 指针)val positionHandle = GLES20.glGetAttribLocation(program, "vPosition")// 开启允许对变量读写,与关闭方法成对出现GLES20.glEnableVertexAttribArray(positionHandle)// 变量地址,数据尺寸,数据类型,是否正常,行跨度,偏移GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, vertexBuffer)val colorHandle = GLES20.glGetUniformLocation(program, "vColor")GLES20.glUniform4fv(colorHandle, 1, colors, 0)// 让 OpenGL 绘制三角形GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)// 关闭对变量读写GLES20.glDisableVertexAttribArray(positionHandle)}

实际上就是把实际的顶点坐标 vertexBuffer 赋值给 GPU 中的 vPosition 变量,将颜色数组写入到 GPU 的 vColor 变量。

运行程序确实画出了一个直角三角形,但是却不是我们想要的等腰直角三角形:

在这里插入图片描述

原因是 OpenGL 绘制的是三维图形,将三维图形绘制到 Android 屏幕这个二维平面上,是需要进行投影的。投影的目的是将三维物体的空间位置和形状转换为屏幕上的二维图像,使观察者可以从适当的视角观察和理解物体。在图形渲染过程中,投影通常是在三维物体的模型空间或世界空间与屏幕空间之间进行的转换。

投影方法主要有两种:

  1. 透视投影(Perspective Projection):透视投影模拟了人眼或相机在观察场景时的视角效果。它产生了近大远小的效果,使离观察者较远的物体显得较小。透视投影常用于创建逼真的三维效果,在虚拟现实、游戏和模拟等领域广泛应用。
  2. 正交投影(Orthographic Projection):正交投影是一种平行投影,保持了物体在不同距离上的大小不变。它独立于观察者的位置和角度,适用于需要保持物体大小和形状一致的场景,如工程制图、CAD 应用和二维游戏等。

投影过程涉及到三个要素:相机(观察物体的位置)、被观察的物体与投影面。

为了让最终看到的是等腰直角三角形,我们需要对顶点着色器中的顶点进行变换,变换方式就是对其左乘矩阵:

attribute vec4 vPosition;
uniform mat4 vMatrix;
void main() {gl_Position=vMatrix*vPosition;
}

现在问题转移到 vMatrix 是如何计算的。首先,我们让 GLRender 将 Surface 的变化同步给 Triangle:

	override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {triangle.onSurfaceChanged(gl, width, height)}

在 Triangle 的 onSurfaceChanged() 中设置投影矩阵、相机矩阵,并计算出变换矩阵:

	private val viewMatrix = FloatArray(16)private val projectMatrix = FloatArray(16)private val mvpMatrix = FloatArray(16)fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {// 计算宽高比val ratio: Float = width.toFloat() / height// 设置投影矩阵Matrix.frustumM(projectMatrix, 0, -ratio, ratio, -1f, 1f, 3f, 120f)// 计算相机矩阵Matrix.setLookAtM(viewMatrix, 0,0f, 0f, 7f, // 摄像机的坐标0f, 0f, 0f, // 目标物的中心坐标0f, 1f, 0f // 相机看目标物的方向)// 计算变换矩阵,将 projectMatrix 与 viewMatrix 相乘Matrix.multiplyMM(mvpMatrix, 0, projectMatrix, 0, viewMatrix, 0)}

更新顶点着色器字符串并向 GPU 传入刚刚计算出的变换矩阵:

	private val vertexShaderCode = "attribute vec4 vPosition;\n" +"uniform mat4 vMatrix;\n" +"void main() {\n" +"    gl_Position=vMatrix*vPosition;\n" +"}"fun onDrawFrame(gl: GL10?) {// 渲染GLES20.glUseProgram(program)// 获取 GPU 中 vMatrix 变量的地址,并将 mvpMatrix 赋值给 vMatrixval matrixHandle = GLES20.glGetUniformLocation(program, "vMatrix")GLES20.glUniformMatrix4fv(matrixHandle, 1, false, mvpMatrix, 0)// 获取 GPU 中 vPosition 变量的地址(实际上是一个 Native 指针)val positionHandle = GLES20.glGetAttribLocation(program, "vPosition")// 开启允许对变量读写,与关闭方法成对出现GLES20.glEnableVertexAttribArray(positionHandle)// 变量地址,数据尺寸,数据类型,是否正常?,行跨度,偏移GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, vertexBuffer)val colorHandle = GLES20.glGetUniformLocation(program, "vColor")GLES20.glUniform4fv(colorHandle, 1, colors, 0)GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3)// 关闭对变量读写GLES20.glDisableVertexAttribArray(positionHandle)}

这样就能画出一个等腰直角三角形:

在这里插入图片描述

最后总结一下绘制步骤:

  • 创建顶点数组
  • 创建顶点着色器和片元着色器
  • 将上层定义的顶点数组和颜色数组通过 OpenGL ES 提供的 API 接口传递给 GL 语言的变量

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/831039.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PySpark学习---销售情况数据统计分析案例

需求分析&#xff1a; 某公司是做零售相关业务&#xff0c;旗下出品各类收银机. 目前公司的收银机已经在全国铺开,在各个省份均有店铺使用.机器是联网的,每一次使用都会将售卖商品数据上传到公司后台.老板现在想对省份维度的销售情况进行统计分析 逻辑需求&#xff1a; 1.各省销…

数据结构与算法实验题五道 A一元多项式的求导 B还原二叉树 C 六度空间 D 基于词频的文件相似度 E 模拟excel排序

A (1) 输入格式说明&#xff1a; 以指数递降方式输入多项式非零项系数和指数&#xff08;绝对值均为不超过1000的整数&#xff09;。数字间以空格分隔。 (2) 输出格式说明&#xff1a; 以与输入相同的格式输出导数多项式非零项的系数和指数。数字间以空格分隔&#xff0c;但…

第八篇:隔离即力量:Python虚拟环境的终极指南

隔离即力量&#xff1a;Python虚拟环境的终极指南 1 引言 在编程的多元宇宙中&#xff0c;Python语言犹如一颗闪耀的星辰&#xff0c;其魅力不仅仅在于简洁的语法&#xff0c;更在于其庞大而繁荣的生态系统。然而&#xff0c;随着应用的增长和复杂性的提升&#xff0c;开发者们…

ChatGPT 记忆功能上线 能记住你和GPT互动的所有内容

你和ChatGPT的互动从今天开始变得更加智能&#xff01;ChatGPT现在可以记住你的偏好和对话细节&#xff0c;为你提供更加相关的回应。和它聊天&#xff0c;你可以教它记住新的东西&#xff0c;例如&#xff1a;“记住我是素食主义者&#xff0c;当你推荐食谱时。”想了解ChatGP…

【15】Head First Java 学习笔记

HeadFirst Java 本人有C语言基础&#xff0c;通过阅读Java廖雪峰网站&#xff0c;简单速成了java&#xff0c;但对其中一些入门概念有所疏漏&#xff0c;阅读本书以弥补。 第一章 Java入门 第二章 面向对象 第三章 变量 第四章 方法操作实例变量 第五章 程序实战 第六章 Java…

Java基于微信小程序+uniapp的校园失物招领小程序(V3.0)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

微软如何打造数字零售力航母系列科普06 - 如何使用微软的Copilot人工智能

如何使用微软的Copilot人工智能&#xff1f; Copilot和ChatGPT有很多相似之处&#xff0c;但微软的聊天机器人本身就有一定的优势。以下是如何对其进行旋转&#xff0c;并查看其最引人注目的功能。 ​​​​​​​ &#xff08;资料来源&#xff1a;Lance Whitney/微软&…

NLP 笔记:TF-IDF

TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff0c;词频-逆文档频率&#xff09;是一种用于信息检索和文本挖掘的统计方法&#xff0c;用来评估一个词在一组文档中的重要性。TF-IDF的基本思想是&#xff0c;如果某个词在一篇文档中出现频率高&#xff0…

使用c++类模板和迭代器进行List模拟实现

List 一、创建节点结构二、创建迭代器类1、类的结构2、一系列的运算符重载 三、创建list1、细节把握2、迭代器函数3、构造函数和析构函数4、增删查改的成员函数 一、创建节点结构 template <class T>//节点结构 struct ListNode {ListNode<T>* _next;ListNode<…

Springboot+vue+小程序+基于微信小程序的在线学习平台

一、项目介绍    基于Spring BootVue小程序的在线学习平台从实际情况出发&#xff0c;结合当前年轻人的学习环境喜好来开发。基于Spring BootVue小程序的在线学习平台在语言上使用Java语言进行开发&#xff0c;在数据库存储方面使用的MySQL数据库&#xff0c;开发工具是IDEA。…

APScheduler定时器使用:django中使用apscheduler,使用mysql做存储后端

一、基本环境 python版本&#xff1a;3.8.5 APScheduler3.10.4 Django3.2.7 djangorestframework3.15.1 SQLAlchemy2.0.29 PyMySQL1.1.0二、django基本设置 2.1、新增一个app 该app用来写apscheduler相关的代码 python manage.py startapp gs_scheduler 2.2、修改配置文件s…

Ollamallama

Olllama 直接下载ollama程序&#xff0c;安装后可在cmd里直接运行大模型&#xff1b; llama 3 meta 开源的最新llama大模型&#xff1b; 下载运行 1 ollama ollama run llama3 2 github 下载仓库&#xff0c;需要linux环境&#xff0c;windows可使用wsl&#xff1b; 接…

C++浮点数format时的舍入问题

C浮点数format时的舍入问题 首先有这样一段代码&#xff1a; #include <iostream> #include <stdio.h> using namespace std;int main() {cout << " main begin : " << endl;printf("%.0f \r\n", 1.5);printf("%.0f \r\n&…

吴恩达2022机器学习专项课程(一)8.2 解决过拟合

目录 解决过拟合&#xff08;一&#xff09;&#xff1a;增加数据解决过拟合&#xff08;二&#xff09;&#xff1a;减少特征特征选择缺点 解决过拟合&#xff08;三&#xff09;&#xff1a;正则化总结 解决过拟合&#xff08;一&#xff09;&#xff1a;增加数据 收集更多训…

【c++】模板编程解密:C++中的特化、实例化和分离编译

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本篇文章我们来学习模版的进阶部分 目录 1.非类型模版参数按需实例化 2.模版的特化函数模版特化函数模版的特化类模版全特化偏特化 3.分离编译模版分离编译 1.非类…

综合性练习(后端代码练习4)——图书管理系统

目录 一、准备工作 二、约定前后端交互接口 1、需求分析 2、接口定义 &#xff08;1&#xff09;登录接口 &#xff08;2&#xff09;图书列表接口 三、服务器代码 &#xff08;1&#xff09;创建一个UserController类&#xff0c;实现登录验证接口 &#xff…

网络应用层之(6)L2TP协议详解

网络应用层之(6)L2TP协议 Author: Once Day Date: 2024年5月1日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文档可参考专栏&#xff1a;通信网络技术_Once-Day的…

Apollo Dreamview+之播放离线数据包

前提条件 完成 Dreamview 插件安装&#xff0c;参见 Studio 插件安装 。 操作步骤 您可以通过包管理和源码两种方式快速体验离线数据包播放操作。其中进入 docker 环境和启动 dreamview 的命令有所区别&#xff0c;请您按照命令进行操作。 步骤一&#xff1a;启动并打开 Dr…

C++学习第十四课:运算符类型与运算符重载

C学习第十四课&#xff1a;运算符类型与运算符重载 在C中&#xff0c;运算符重载是一种使得自定义类型&#xff08;如类对象&#xff09;能够使用C内建运算符的能力。运算符重载允许程序员定义运算符对用户定义类型的特殊行为&#xff0c;这增加了程序的可读性和自然表达能力。…

PaLmTac嵌入软体手手掌的视触觉传感器

触觉是感知和操作之间的桥梁。触觉信息对于手部行为反馈和规划具有重要意义。软体手的柔性特性在人机交互、生物医学设备和假肢等方面具有潜在应用的优势。本文提出了一种名为 PaLmTac的嵌入软体手手掌的视触觉传感器&#xff08;vision-based tactile sensor, VBTS&#xff09…