使用OpenGL实现窗口中绘制三角形(纯色|彩色)、正方形(变色)
- 一、简单实现窗口绘制三角形
- 二、绘制的多颜色三角形(基于 ‘ 简单实现窗口绘制三角形 ’ )
- 1、在顶点着色器和片段着色器中添加了颜色的输入和输出处理。
- 2、makeVao 函数中启用了顶点位置和颜色的属性,并分别设置了它们的属性指针。
- 3、draw 函数中修改了绘制的顶点数量计算,因为现在每个顶点包含位置和颜色共 6 个值。
- 三、绘制不停变换颜色三角形/正方形(基于 ‘ 简单实现窗口绘制三角形 ’ )
- 1、如果需要展示三角形那么 triangle 就不动,如若正方形就需要自己调整对应的xyz坐标
- 2、片段着色器中使用时间来计算颜色的变化。
- 3、在draw函数中,获取当前时间并传递给片段着色器的uniform变量。同时,确保在初始化OpenGL时启用垂直同步,以控制渲染速率。
本文只是跟随此 链接 文章简单实现并了解OpenGL,如若你有兴趣可去实现本文衍生的【生命游戏】
在 OpenGL 中,大多数复杂的形状最终都是通过三角形来绘制和构建的,这是因为三角形是最简单且不可再分的多边形,具有良好的稳定性和通用性, 几乎所有的图形都可以通过组合多个三角形来近似表示。
/
例如矩形可以用两个三角形组成,圆形可以通过多个小三角形来逼近。
/
OpenGL 也支持其他基本图元,如点(GL_POINTS)、线(GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP)等。但对于构建复杂的形状,三角形仍然是最常用和基础的元素。所以,虽然不是所有形状都严格地只能用三角形绘制,但在实际应用中,三角形是非常重要和常用的基础构建块。
一、简单实现窗口绘制三角形
// @Author cory 2024/8/1 15:59:00
package openGLimport ("fmt""github.com/go-gl/gl/v4.1-core/gl""github.com/go-gl/glfw/v3.2/glfw""log""runtime""strings"
)const (//定义画布参数width = 600height = 600
)// @Title OneMain 2024/8/1 16:05:00
// @Description 主循环
// @Auth Cory
func OneMain() {//1、运行时包指示给 LockOSThread(),这确保我们将始终在同一操作系统线程中执行,这对于 GLFW 很重要,因为 GLFW 必须始终从初始化它的同一线程中调用runtime.LockOSThread()//2、调用 initGlfw 来获取窗口引用,并延迟终止。然后在for循环中使用窗口引用,在for循环中,我们说只要窗口应该保持打开状态,就做一些事情。window := initGlfw()defer glfw.Terminate()program := initOpenGL() //初始化OpenGL并返回一个初始化的程序。vao := makeVao(triangle) //根据定义的三角形来返回顶点数组对象的指针//3、在当前循环中可以做很多事情for !window.ShouldClose() {// TODO// 检查链接状态var status int32gl.GetProgramiv(program, gl.LINK_STATUS, &status)if status == gl.FALSE {var logLength int32gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength)log := strings.Repeat("\x00", int(logLength+1))gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log))fmt.Println("链接程序错误:", log)continue}draw(vao, window, program)}
}// @Title initGlfw 2024/8/1 16:04:00
// @Description 创建窗口
// @Auth Cory
// @Return error ---> "返回窗口"
func initGlfw() *glfw.Window {//1、初始化 GLFW 包err := glfw.Init()if err != nil {panic(err)}//2、定义一些全局 GLFW 属性glfw.WindowHint(glfw.Resizable, glfw.False) //指定用户是否可以调整窗口的大小。glfw.WindowHint(glfw.ContextVersionMajor, 4) //指定创建的上下文必须与之兼容的客户端API版本。OR 2glfw.WindowHint(glfw.ContextVersionMinor, 1) //指定创建的上下文必须与之兼容的客户端API版本。glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) //指定要为哪个OpenGL配置文件创建上下文。硬约束。glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) //指定OpenGL上下文是否应该是向前兼容的。硬约束。//3、创建一个 glfw (就是我们要进行未来绘画的地方,只需告诉它我们想要的宽度和高度,以及标题,然后调用 window.MakeContextCurrent,将窗口绑定到我们当前的线程。最后返回窗口)window, err := glfw.CreateWindow(width, height, "铁憨憨_自学GL", nil, nil)if err != nil {panic(err)}window.MakeContextCurrent()return window
}// compileShader 此函数的用途是以字符串及其类型的形式接收着色器源代码,并返回指向生成的编译着色器的指针。如果它编译失败,我们将返回一个包含详细信息的错误。
func compileShader(source string, shaderType uint32) (uint32, error) {shader := gl.CreateShader(shaderType)csources, free := gl.Strs(source)gl.ShaderSource(shader, 1, csources, nil)free()gl.CompileShader(shader)var status int32gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)if status == gl.FALSE {var logLength int32gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)log := strings.Repeat("\x00", int(logLength+1))gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))return 0, fmt.Errorf("failed to compile %v: %v", source, log)}return shader, nil
}/*
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
============================================================================================================================================================================================================
*/const (vertexShaderSource = `#version 410in vec3 vp;void main() {gl_Position = vec4(vp, 1.0);}` + "\x00" // GLSL 源代码 顶点着色器fragmentShaderSource = `#version 410out vec4 frag_colour;void main() {frag_colour = vec4(241.0/255.0, 148.0/255.0, 138.0/255.0, 1.0);}` + "\x00" // GLSL 源代码 片段着色器
)// 三角形参数
var (triangle = []float32{0, 0, 0, // top-1, 0, 0, // left0, -1, 0, // right}
)// initOpenGL 初始化OpenGL并返回一个初始化的程序。
func initOpenGL() uint32 {// 1、初始化 OpenGL 库if err := gl.Init(); err != nil {panic(err)}// 2、获取 OpenGL 版本信息并打印日志version := gl.GoStr(gl.GetString(gl.VERSION))log.Println("OpenGL version", version)// 3、编译顶点着色器vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)if err != nil {panic(err)}// 4、编译片段着色器fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)if err != nil {panic(err)}// 5、创建一个 OpenGL 程序对象prog := gl.CreateProgram()// 将顶点着色器和片段着色器附加到程序对象上gl.AttachShader(prog, vertexShader)gl.AttachShader(prog, fragmentShader)// 链接程序对象gl.LinkProgram(prog)// 6、返回链接后的程序对象的标识符return prog
}// makeVao 从提供的点初始化并返回顶点数组。
// 解释如下:
// 1、makeVao 函数的主要目的是创建和配置一个用于存储顶点数据的顶点数组对象(Vertex Array Object,简称 VAO)
// 2、points 数组包含了定义图形(例如三角形)的顶点坐标数据。通过一系列的 OpenGL 函数调用,将这些顶点数据存储在缓冲区对象(VBO)中,并将 VBO 与 VAO 进行关联和配置。
// 3、此处的uint32实际上是一个无符号的标识符,用来指向已经配置好的顶点数组对象,在后续的渲染代码中,可以通过 gl.BindVertexArray(vao) 来绑定这个创建好的 VAO,然后执行绘制操作
func makeVao(points []float32) uint32 {var vbo uint32gl.GenBuffers(1, &vbo)gl.BindBuffer(gl.ARRAY_BUFFER, vbo)gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)var vao uint32gl.GenVertexArrays(1, &vao)gl.BindVertexArray(vao)gl.EnableVertexAttribArray(0)gl.BindBuffer(gl.ARRAY_BUFFER, vbo)gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)return vao
}func draw(vao uint32, window *glfw.Window, program uint32) {// 1、清除颜色缓冲区和深度缓冲区,为新的绘制操作准备干净的画布gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)// 2、使用指定的程序进行后续的绘制操作gl.UseProgram(program)// 3、绑定之前创建的顶点数组对象(VAO)gl.BindVertexArray(vao)// 4、使用 `gl.DrawArrays` 函数进行绘制// `gl.TRIANGLES` 表示绘制三角形// `0` 表示从顶点数组的起始位置开始绘制// `int32(len(triangle)/3)` 表示要绘制的顶点数量,这里假设 `triangle` 存储了顶点数据,并且每 3 个顶点构成一个三角形gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3))// 5、让 GLFW 检查是否有鼠标或键盘等事件glfw.PollEvents()// 6、交换前后缓冲区,将之前在后台缓冲区绘制的内容显示到屏幕上window.SwapBuffers()
}
二、绘制的多颜色三角形(基于 ‘ 简单实现窗口绘制三角形 ’ )
1、在顶点着色器和片段着色器中添加了颜色的输入和输出处理。
const (vertexShaderSource = `#version 410in vec3 vp;in vec3 color; out vec3 fragColor; void main() {gl_Position = vec4(vp, 1.0);fragColor = color; }` + "\x00" // GLSL 源代码 顶点着色器fragmentShaderSource = `#version 410in vec3 fragColor; out vec4 outColor;void main() {outColor = vec4(fragColor, 1.0); }` + "\x00" // GLSL 源代码 片段着色器
)// 三角形参数,包含位置和颜色(前三xyz,后三rgb)
var (triangle = []float32{0, 0, 0, 1.0, 0.0, 0.0, // 红色顶点-1, 0, 0, 0.0, 1.0, 0.0, // 绿色顶点0, -1, 0, 0.0, 0.0, 1.0, // 蓝色顶点}
)
2、makeVao 函数中启用了顶点位置和颜色的属性,并分别设置了它们的属性指针。
gl.EnableVertexAttribArray(0) 修改为:
// 启用顶点位置和颜色属性gl.EnableVertexAttribArray(0)gl.EnableVertexAttribArray(1)
此处为什么要两个EnableVertexAttribArray,为什么不能一步到位,直接要设置1的哪个?
因为在这个场景中,不能只使用 gl.EnableVertexAttribArray(1) 而省略 gl.EnableVertexAttribArray(0)
原因如下:
在您的顶点着色器中,定义了两个输入属性:顶点位置 vp 和顶点颜色 color 。
gl.EnableVertexAttribArray 用于启用顶点属性数组,以便在渲染时能够使用它们。
gl.EnableVertexAttribArray(0) 启用的是 顶点位置 属性,
gl.EnableVertexAttribArray(1) 启用的是 顶点颜色 属性。
如果只启用 1 而不启用 0 ,那么顶点位置属性将不会被激活,导致在渲染时无法正确获取和使用顶点的位置信息,从而可能导致图形无法正确显示。每个顶点属性都需要单独启用,以确保 OpenGL 知道在渲染时应该从相应的缓冲区中获取和使用这些数据。
然后在 gl.VertexAttribPointer 后面追加
// 为顶点颜色设置属性指针gl.VertexAttribPointer(1, 3, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))
3、draw 函数中修改了绘制的顶点数量计算,因为现在每个顶点包含位置和颜色共 6 个值。
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3)) 修改为:
// `int32(len(triangle)/6)` 表示要绘制的顶点数量,每个顶点包含位置和颜色gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/6))
三、绘制不停变换颜色三角形/正方形(基于 ‘ 简单实现窗口绘制三角形 ’ )
1、如果需要展示三角形那么 triangle 就不动,如若正方形就需要自己调整对应的xyz坐标
切片包含 9 个值,三角形的每个顶点对应 3 个值。顶线 0、0.5、0 是表示为 X、Y 和 Z 坐标的顶点,第二条线是左顶点,第三条线是右顶点。这三对中的每一对都表示顶点相对于窗口中心的 X、Y 和 Z 坐标,介于 -1 和 1 之间。因此,最高点的 X 为零,因为它的 X 位于窗口的中心,Y 为 0.5 表示它将相对于窗口中心向上移动四分之一(因为范围为 -1 比 1),Z 为零。
–
正方形为两个三角形组合
triangle = []float32{0, 0, 0, // top-1, 0, 0, // left0, -1, 0, // right-1, 0, 0, // top-1, -1, 0, // left0, -1, 0, // right}
2、片段着色器中使用时间来计算颜色的变化。
//使用时间信息来动态计算颜色。这里使用正弦和余弦函数来实现周期性的颜色变化效果。fragmentShaderSource = `#version 410out vec4 frag_colour;uniform float time;void main() {// 根据时间计算颜色float r = sin(time) * 0.5 + 0.5;float g = cos(time) * 0.5 + 0.5;float b = 0.5;frag_colour = vec4(r, g, b, 1.0);}` + "\x00"
3、在draw函数中,获取当前时间并传递给片段着色器的uniform变量。同时,确保在初始化OpenGL时启用垂直同步,以控制渲染速率。
// 获取当前时间(秒为单位)time := float32(glfw.GetTime())// 获取uniform变量的位置timeUniform := gl.GetUniformLocation(program, gl.Str("time\x00"))// 将时间值传递给片段着色器的uniform变量gl.Uniform1f(timeUniform, time)
或者自己来调整颜色变换的速度
此时 fragmentShaderSource 就不能是常量了,因为多了一个speed来控制速度
fragmentShaderSource = `#version 410out vec4 frag_colour;uniform float time;uniform float speed;void main() {// 根据时间和速度计算颜色float r = sin(time * speed) * 0.5 + 0.5;float g = cos(time * speed) * 0.5 + 0.5;float b = 0.5;frag_colour = vec4(r, g, b, 1.0);}` + "\x00" // GLSL 源代码 片段着色器
// 获取当前时间(秒为单位)time := float32(glfw.GetTime())// 获取uniform变量的位置timeUniform := gl.GetUniformLocation(program, gl.Str("time\x00"))// 获取 speed uniform 变量的位置speedUniform := gl.GetUniformLocation(program, gl.Str("speed\x00"))// 将时间值传递给片段着色器的uniform变量gl.Uniform1f(timeUniform, time)// 设置 speed 的值 (数越大变换的速度越快)gl.Uniform1f(speedUniform, 2.0)