使用OpenGL实现窗口中设置四个顶点来渲染正方形,然后进行纹理贴图
一、代码实现
// @Author cory 2024/8/1 15:59:00 package openGL import ( "fmt" "github.com/go-gl/gl/v4.1-core/gl" "github.com/go-gl/glfw/v3.2/glfw" "image" glDraw "image/draw" "image/jpeg" "log" "os" "runtime" "strings" ) const ( //定义画布参数 width = 600 height = 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, texture := initOpenGL() // 初始化 OpenGL 并返回程序和纹理 vao := makeVao(triangle) // 根据定义的三角形来返回顶点数组对象的指针 //3、在当前循环中可以做很多事情 for !window.ShouldClose() { // TODO // 检查链接状态 var status int32 gl.GetProgramiv(program, gl.LINK_STATUS, &status) if status == gl.FALSE { var logLength int32 gl.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, texture) // 传递纹理 } } // @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 2 glfw.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 int32 gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status) if status == gl.FALSE { var logLength int32 gl.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 410 in vec3 vp; in vec2 texCoord; // 新增:纹理坐标 out vec2 TexCoord; // 输出纹理坐标到片段着色器 void main() { gl_Position = vec4(vp, 1.0); TexCoord = texCoord; }` + "\x00" // GLSL 源代码 顶点着色器 fragmentShaderSource = ` #version 410 in vec2 TexCoord; // 接收顶点着色器传递的纹理坐标 uniform sampler2D ourTexture; // 纹理采样器 out vec4 frag_colour; void main() { frag_colour = texture(ourTexture, TexCoord); // 进行纹理采样 }` + "\x00" // GLSL 源代码 片段着色器 ) // 正方形 var ( triangle = []float32{ -0.5, 0.5, 0.0, 0.0, 0.0, // top left 0.5, 0.5, 0.0, 1.0, 0.0, // top right -0.5, -0.5, 0.0, 0.0, 1.0, // bottom left 0.5, -0.5, 0.0, 1.0, 1.0, // bottom right } ) // initOpenGL 初始化 OpenGL 并返回程序和纹理 func initOpenGL() (uint32, 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() // 6、将顶点着色器和片段着色器附加到程序对象上 gl.AttachShader(prog, vertexShader) gl.AttachShader(prog, fragmentShader) // 7、链接程序对象 gl.LinkProgram(prog) // 生成纹理 texture, err := loadTexture("C:\\Users\\MSI\\Pictures\\timg.jpg") if err != nil { panic(err) } // 获取 uniform 变量的位置 textureLocation := gl.GetUniformLocation(prog, gl.Str("ourTexture\x00")) // 设置 uniform 变量的值为 0(纹理单元 0) gl.Uniform1i(textureLocation, 0) // 启用纹理单元 0 gl.ActiveTexture(gl.TEXTURE0) // 将纹理绑定到纹理单元 0 gl.BindTexture(gl.TEXTURE_2D, texture) // 8、返回链接后的程序对象的标识符和纹理标识符 return prog, texture } // 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 uint32 gl.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 uint32 gl.GenVertexArrays(1, &vao) gl.BindVertexArray(vao) gl.EnableVertexAttribArray(0) gl.EnableVertexAttribArray(1) // 启用纹理坐标属性 gl.BindBuffer(gl.ARRAY_BUFFER, vbo) gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 5*4, gl.PtrOffset(0)) // 顶点位置 gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5*4, gl.PtrOffset(3*4)) // 纹理坐标 return vao } // @Title loadTexture 2024/8/2 16:35:00 // @Description 加载纹理 // @Auth Cory // @param fileName ---> "文件名称" // @Return uint32 ---> "标识符" // @Return error ---> "错误信息" func loadTexture(fileName string) (uint32, error) { //1、尝试打开指定的图像文件,如果打开失败则返回错误。 imgFile, err := os.Open(fileName) if err != nil { return 0, fmt.Errorf("texture %q not found on disk: %v", fileName, err) } defer imgFile.Close() //2、使用 jpeg 解码器对打开的文件进行解码,获取图像数据。 img, err := jpeg.Decode(imgFile) if err != nil { return 0, err } //3、创建一个新的 RGBA 图像来存储解码后的图像数据,确保数据格式统一。 rgba := image.NewRGBA(img.Bounds()) if rgba.Stride != rgba.Rect.Size().X*4 { //检查新创建的 RGBA 图像的步长是否符合预期,如果不符合则返回错误。 return 0, fmt.Errorf("unsupported stride") } glDraw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, glDraw.Src) var texture uint32 gl.GenTextures(1, &texture) //生成一个新的纹理对象。 gl.ActiveTexture(gl.TEXTURE0) //激活纹理单元 0。 gl.BindTexture(gl.TEXTURE_2D, texture) //将生成的纹理对象绑定到当前上下文的 2D 纹理位置。 //用于设置纹理的参数,如滤波方式(TEXTURE_MIN_FILTER 和 TEXTURE_MAG_FILTER 为线性滤波)、环绕方式(TEXTURE_WRAP_S 和 TEXTURE_WRAP_T 为重复)。 gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) //用于将图像数据加载到纹理对象中,指定了纹理的内部格式(gl.RGBA)、图像的宽度、高度、边界等参数。 gl.TexImage2D( gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Size().X), int32(rgba.Rect.Size().Y), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) return texture, nil } func draw(vao uint32, window *glfw.Window, program uint32, texture 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)/5)` 表示要绘制的顶点数量,这里假设 `triangle` 存储了顶点数据和纹理坐标,并且每 5 个元素构成一个顶点 gl.DrawArrays(gl.TRIANGLE_STRIP, 0, int32(len(triangle)/5)) // 5、让 GLFW 检查是否有鼠标或键盘等事件 glfw.PollEvents() // 6、交换前后缓冲区,将之前在后台缓冲区绘制的内容显示到屏幕上 window.SwapBuffers() }
二、代码讲解
1、triangl 变量
下方采用四个顶点来绘制正方形,每个顶点有5个参数,其中前三个为xyz,后两个为纹理坐标。
这里主要说一下纹理坐标:
(0.0, 0.0) 表示纹理的左下角,(1.0, 1.0) 表示纹理的右上角
(0.0, 1.0) 表示纹理的左上角,(1.0, 0.0) 表示纹理的右下角
var ( triangle = []float32{ -0.5, 0.5, 0.0, 0.0, 0.0, // top left 0.5, 0.5, 0.0, 1.0, 0.0, // top right -0.5, -0.5, 0.0, 0.0, 1.0, // bottom left 0.5, -0.5, 0.0, 1.0, 1.0, // bottom right } )
如若4个顶点绘制正方形需要注意一下代码的 TRIANGLE_STRIP 和 TRIANGLES 的区别
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, int32(len(triangle)/5))
2、纹理单元的值
讲解1:常见的 OpenGL 实现中,通常支持多个纹理单元,其索引通常从 0 开始。
代码中,使用了 gl.ActiveTexture(gl.TEXTURE0) 激活了纹理单元 0 ,并将纹理绑定到这个单元。
如果您要使用多个纹理,需要激活不同的纹理单元,例如 gl.ActiveTexture(gl.TEXTURE1) 表示激活纹理单元 1 ,依此类推。
但要注意,所使用的纹理单元索引应在你的 OpenGL 上下文所支持的范围内,并且在着色器中也需要正确设置相应的纹理采样器来对应您激活和绑定的纹理单元。
// 生成纹理 texture, err := loadTexture("C:\\Users\\MSI\\Pictures\\timg.jpg") if err != nil { panic(err) } // 获取 uniform 变量的位置 textureLocation := gl.GetUniformLocation(prog, gl.Str("ourTexture\x00")) // 设置 uniform 变量的值为 0(纹理单元 0) gl.Uniform1i(textureLocation, 0) // 启用纹理单元 0 gl.ActiveTexture(gl.TEXTURE0) // 将纹理绑定到纹理单元 0 gl.BindTexture(gl.TEXTURE_2D, texture)
讲解2:纹理单元数量通常是有限,一般来说,至少会支持 8 到 32 个纹理单元
3、为什么会有两个gl.EnableVertexAttribArray?
有两个 gl.EnableVertexAttribArray 调用,分别是 :
gl.EnableVertexAttribArray(0)和 gl.EnableVertexAttribArray(1)
|
这是因为顶点数据包含了两种不同的属性:顶点位置和纹理坐标。
gl.EnableVertexAttribArray(0) 用于启用顶点位置属性
gl.EnableVertexAttribArray(1) 用于启用纹理坐标属性。
|
如果只使用 gl.EnableVertexAttribArray(1) 而不启用 0 ,那么顶点位置属性将不会被激活和使用,这会导致图形无法正确绘制,因为顶点位置是确定图形形状和位置的关键属性。
gl.EnableVertexAttribArray(0) gl.EnableVertexAttribArray(1) // 启用纹理坐标属性
在之前写过的 Golang之OpenGL(一) 中 绘制多颜色三角形 也提到过这个。
那他是如何区 顶点位置和颜色属性 还是 顶点位置和纹理坐标
|
在 gl.VertexAttribPointer 中为第一个启用的属性(索引为 0)设置的步长和偏移量与顶点位置数据的布局相匹配,并且为第二个启用的属性(索引为 1)设置的步长和偏移量与颜色数据的布局相匹配,那么就是启用了顶点位置和颜色属性。相反,如果为第二个启用的属性(索引为 1)设置的步长和偏移量与纹理坐标数据的布局相匹配,那么就是启用了顶点位置和纹理坐标。
|
关键在于 gl.VertexAttribPointer 函数对每个属性的具体设置,包括数据类型、步长、偏移量等,这些设置决定了每个启用的属性对应的数据类型是颜色还是纹理坐标。
Golang之OpenGL(一):绘制多颜色三角形
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 6*4, nil) //顶点位置设置属性 gl.VertexAttribPointer(1, 3, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))// 顶点颜色设置属性指针
Golang之OpenGL(二):纹理贴图
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 5*4, gl.PtrOffset(0)) // 顶点位置设置属性 gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5*4, gl.PtrOffset(3*4)) // 纹理坐标设置属性