分享

OpenGL 与 Go 教程(二)绘制游戏面板

 KyunraWang 2018-05-08


OpenGL 与 Go 教程(二)绘制游戏面板

2017-10-07 22:35       

这篇教程的所有源代码都可以在 GitHub 上找到。

欢迎回到《OpenGL 与 Go 教程》。如果你还没有看过第一节,那就要回过头去看看那一节。

你现在应该能够创造一个漂亮的白色三角形,但我们不会把三角形当成我们游戏的基本单元,是时候把三角形变成正方形了,然后我们会做出一个完整的方格。

让我们现在开始做吧!

利用三角形绘制方形

在我们绘制方形之前,先把三角形变成直角三角形。打开 main.go 文件,把 triangle 的定义改成像这个样子:

  1. triangle = []float32{
  2. -0.5, 0.5, 0,
  3. -0.5, -0.5, 0,
  4. 0.5, -0.5, 0,
  5. }

我们做的事情是,把最上面的顶点 X 坐标移动到左边(也就是变为 -0.5),这就变成了像这样的三角形:

Conway's Game of Life  - 右弦三角形

Conway's Game of Life - 右弦三角形

很简单,对吧?现在让我们用两个这样的三角形顶点做成正方形。把 triangle 重命名为 square,然后添加第二个倒置的三角形的顶点数据,把直角三角形变成这样的:

  1. square = []float32{
  2. -0.5, 0.5, 0,
  3. -0.5, -0.5, 0,
  4. 0.5, -0.5, 0,
  5. -0.5, 0.5, 0,
  6. 0.5, 0.5, 0,
  7. 0.5, -0.5, 0,
  8. }

注意:你也要把在 main 和 draw 里面命名的 triangle 改为 square

我们通过添加三个顶点,把顶点数增加了一倍,这三个顶点就是右上角的三角形,用来拼成方形。运行它看看效果:

Conway's Game of Life - 两个三角形构成方形

Conway's Game of Life - 两个三角形构成方形

很好,现在我们能够绘制正方形了!OpenGL 一点都不难,对吧?

在窗口中绘制方形格子

现在我们能画一个方形,怎么画 100 个吗?我们来创建一个 cell 结构体,用来表示格子的每一个单元,因此我们能够很灵活的选择绘制的数量:

  1. type cell struct {
  2. drawable uint32
  3. x int
  4. y int
  5. }

cell 结构体包含一个 drawable 属性,这是一个顶点数组对象,就像我们在之前创建的一样,这个结构体还包含 X 和 Y 坐标,用来表示这个格子的位置。

我们还需要两个常量,用来设定格子的大小和形状:

  1. const (
  2. ...
  3. rows = 10
  4. columns = 10
  5. )

现在我们添加一个创建格子的函数:

  1. func makeCells() [][]*cell {
  2. cells := make([][]*cell, rows, rows)
  3. for x := 0; x < rows; x++ {
  4. for y := 0; y < columns; y++ {
  5. c := newCell(x, y)
  6. cells[x] = append(cells[x], c)
  7. }
  8. }
  9. return cells
  10. }

这里我们创建多维的切片slice,代表我们的游戏面板,用名为 newCell 的新函数创建的 cell 来填充矩阵的每个元素,我们待会就来实现 newCell 这个函数。

在接着往下阅读前,我们先花一点时间来看看 makeCells 函数做了些什么。我们创造了一个切片,这个切片的长度和格子的行数相等,每一个切片里面都有一个细胞cell的切片,这些细胞的数量与列数相等。如果我们把 rows 和 columns 都设定成 2,那么就会创建如下的矩阵:

  1. [
  2. [cell, cell],
  3. [cell, cell]
  4. ]

还可以创建一个更大的矩阵,包含 10x10 个细胞:

  1. [
  2. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  3. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  4. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  5. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  6. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  7. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  8. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  9. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  10. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
  11. [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell]
  12. ]

现在应该理解了我们创造的矩阵的形状和表示方法。让我们看看 newCell 函数到底是怎么填充矩阵的:

  1. func newCell(x, y int) *cell {
  2. points := make([]float32, len(square), len(square))
  3. copy(points, square)
  4. for i := 0; i < len(points); i++ {
  5. var position float32
  6. var size float32
  7. switch i % 3 {
  8. case 0:
  9. size = 1.0 / float32(columns)
  10. position = float32(x) * size
  11. case 1:
  12. size = 1.0 / float32(rows)
  13. position = float32(y) * size
  14. default:
  15. continue
  16. }
  17. if points[i] < 0 {
  18. points[i] = (position * 2) - 1
  19. } else {
  20. points[i] = ((position + size) * 2) - 1
  21. }
  22. }
  23. return &cell{
  24. drawable: makeVao(points),
  25. x: x,
  26. y: y,
  27. }
  28. }

这个函数里有很多内容,我们把它分成几个部分。我们做的第一件事是复制了 square 的定义。这让我们能够修改该定义,定制当前的细胞位置,而不会影响其它使用 square 切片定义的细胞。然后我们基于当前索引迭代 points 副本。我们用求余数的方法来判断我们是在操作 X 坐标(i % 3 == 0),还是在操作 Y 坐标(i % 3 == 1)(跳过 Z 坐标是因为我们仅在二维层面上进行操作),跟着确定细胞的大小(也就是占据整个游戏面板的比例),当然它的位置是基于细胞在 相对游戏面板的 X 和 Y 坐标。

接着,我们改变那些包含在 square 切片中定义的 0.50, -0.5 这样的点。如果点小于 0,我们就把它设置成原来的 2 倍(因为 OpenGL 坐标的范围在 -1 到 1 之间,范围大小是 2),减 1 是为了归一化 OpenGL 坐标。如果点大于等于 0,我们的做法还是一样的,不过要加上我们计算出的尺寸。

这样做是为了设置每个细胞的大小,这样它就能只填充它在面板中的部分。因为我们有 10 行 10 列,每一个格子能分到游戏面板的 10% 宽度和高度。

最后,确定了所有点的位置和大小,我们用提供的 X 和 Y 坐标创建一个 cell,并设置 drawable 字段与我们刚刚操作 points 得到的顶点数组对象(vao)一致。

好了,现在我们在 main 函数里可以移去对 makeVao 的调用了,用 makeCells 代替。我们还修改了 draw,让它绘制一系列的细胞而不是一个 vao

  1. func main() {
  2. ...
  3. // vao := makeVao(square)
  4. cells := makeCells()
  5. for !window.ShouldClose() {
  6. draw(cells, window, program)
  7. }
  8. }
  9. func draw(cells [][]*cell, window *glfw.Window, program uint32) {
  10. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  11. gl.UseProgram(program)
  12. // TODO
  13. glfw.PollEvents()
  14. window.SwapBuffers()
  15. }

现在我们要让每个细胞知道怎么绘制出自己。在 cell 里面添加一个 draw 函数:

  1. func (c *cell) draw() {
  2. gl.BindVertexArray(c.drawable)
  3. gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square) / 3))
  4. }

这看上去很熟悉,它很像我们之前在 vao 里写的 draw,唯一的区别是我们的 BindVertexArray 函数用的是 c.drawable,这是我们在 newCell 中创造的细胞的 vao

回到 main 中的 draw 函数上,我们可以循环每个细胞,让它们自己绘制自己:

  1. func draw(cells [][]*cell, window *glfw.Window, program uint32) {
  2. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  3. gl.UseProgram(program)
  4. for x := range cells {
  5. for _, c := range cells[x] {
  6. c.draw()
  7. }
  8. }
  9. glfw.PollEvents()
  10. window.SwapBuffers()
  11. }

如你所见,我们循环每一个细胞,调用它的 draw 函数。如果运行这段代码,你能看到像下面这样的东西:

Conway's Game of Life - 全部格子

Conway's Game of Life - 全部格子

这是你想看到的吗?我们做的是在格子里为每一行每一列创建了一个方块,然后给它上色,这就填满了整个面板!

注释掉 for 循环,我们就可以看到一个明显独立的细胞,像这样:

  1. // for x := range cells {
  2. // for _, c := range cells[x] {
  3. // c.draw()
  4. // }
  5. // }
  6. cells[2][3].draw()

Conway's Game of Life - 一个单独的细胞

Conway's Game of Life - 一个单独的细胞

这只绘制坐标在 (X=2, Y=3) 的格子。你可以看到,每一个独立的细胞占据着面板的一小块部分,并且负责绘制自己那部分空间。我们也能看到游戏面板有自己的原点,也就是坐标为 (X=0, Y=0) 的点,在窗口的左下方。这仅仅是我们的 newCell 函数计算位置的方式,也可以用右上角,右下角,左上角,中央,或者其它任何位置当作原点。

接着往下做,移除 cells[2][3].draw() 这一行,取消 for 循环的那部分注释,变成之前那样全部绘制的样子。

总结

好了,我们现在能用两个三角形画出一个正方形了,我们还有一个游戏的面板了!我们该为此自豪,目前为止我们已经接触到了很多零碎的内容,老实说,最难的部分还在前面等着我们!

在接下来的第三节,我们会实现游戏核心逻辑,看到很酷的东西!

回顾

这是这一部分教程中 main.go 文件的内容:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "runtime"
  6. "strings"
  7. "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
  8. "github.com/go-gl/glfw/v3.2/glfw"
  9. )
  10. const (
  11. width = 500
  12. height = 500
  13. vertexShaderSource = `
  14. #version 410
  15. in vec3 vp;
  16. void main() {
  17. gl_Position = vec4(vp, 1.0);
  18. }
  19. ` + "\x00"
  20. fragmentShaderSource = `
  21. #version 410
  22. out vec4 frag_colour;
  23. void main() {
  24. frag_colour = vec4(1, 1, 1, 1.0);
  25. }
  26. ` + "\x00"
  27. rows = 10
  28. columns = 10
  29. )
  30. var (
  31. square = []float32{
  32. -0.5, 0.5, 0,
  33. -0.5, -0.5, 0,
  34. 0.5, -0.5, 0,
  35. -0.5, 0.5, 0,
  36. 0.5, 0.5, 0,
  37. 0.5, -0.5, 0,
  38. }
  39. )
  40. type cell struct {
  41. drawable uint32
  42. x int
  43. y int
  44. }
  45. func main() {
  46. runtime.LockOSThread()
  47. window := initGlfw()
  48. defer glfw.Terminate()
  49. program := initOpenGL()
  50. cells := makeCells()
  51. for !window.ShouldClose() {
  52. draw(cells, window, program)
  53. }
  54. }
  55. func draw(cells [][]*cell, window *glfw.Window, program uint32) {
  56. gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
  57. gl.UseProgram(program)
  58. for x := range cells {
  59. for _, c := range cells[x] {
  60. c.draw()
  61. }
  62. }
  63. glfw.PollEvents()
  64. window.SwapBuffers()
  65. }
  66. func makeCells() [][]*cell {
  67. cells := make([][]*cell, rows, rows)
  68. for x := 0; x < rows; x++ {
  69. for y := 0; y < columns; y++ {
  70. c := newCell(x, y)
  71. cells[x] = append(cells[x], c)
  72. }
  73. }
  74. return cells
  75. }
  76. func newCell(x, y int) *cell {
  77. points := make([]float32, len(square), len(square))
  78. copy(points, square)
  79. for i := 0; i < len(points); i++ {
  80. var position float32
  81. var size float32
  82. switch i % 3 {
  83. case 0:
  84. size = 1.0 / float32(columns)
  85. position = float32(x) * size
  86. case 1:
  87. size = 1.0 / float32(rows)
  88. position = float32(y) * size
  89. default:
  90. continue
  91. }
  92. if points[i] < 0 {
  93. points[i] = (position * 2) - 1
  94. } else {
  95. points[i] = ((position + size) * 2) - 1
  96. }
  97. }
  98. return &cell{
  99. drawable: makeVao(points),
  100. x: x,
  101. y: y,
  102. }
  103. }
  104. func (c *cell) draw() {
  105. gl.BindVertexArray(c.drawable)
  106. gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
  107. }
  108. // 初始化 glfw,返回一个可用的 Window
  109. func initGlfw() *glfw.Window {
  110. if err := glfw.Init(); err != nil {
  111. panic(err)
  112. }
  113. glfw.WindowHint(glfw.Resizable, glfw.False)
  114. glfw.WindowHint(glfw.ContextVersionMajor, 4)
  115. glfw.WindowHint(glfw.ContextVersionMinor, 1)
  116. glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
  117. glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
  118. window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
  119. if err != nil {
  120. panic(err)
  121. }
  122. window.MakeContextCurrent()
  123. return window
  124. }
  125. // 初始化 OpenGL 并返回一个可用的着色器程序
  126. func initOpenGL() uint32 {
  127. if err := gl.Init(); err != nil {
  128. panic(err)
  129. }
  130. version := gl.GoStr(gl.GetString(gl.VERSION))
  131. log.Println("OpenGL version", version)
  132. vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
  133. if err != nil {
  134. panic(err)
  135. }
  136. fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
  137. if err != nil {
  138. panic(err)
  139. }
  140. prog := gl.CreateProgram()
  141. gl.AttachShader(prog, vertexShader)
  142. gl.AttachShader(prog, fragmentShader)
  143. gl.LinkProgram(prog)
  144. return prog
  145. }
  146. // 初始化并返回由 points 提供的顶点数组
  147. func makeVao(points []float32) uint32 {
  148. var vbo uint32
  149. gl.GenBuffers(1, &vbo)
  150. gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
  151. gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)
  152. var vao uint32
  153. gl.GenVertexArrays(1, &vao)
  154. gl.BindVertexArray(vao)
  155. gl.EnableVertexAttribArray(0)
  156. gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
  157. gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
  158. return vao
  159. }
  160. func compileShader(source string, shaderType uint32) (uint32, error) {
  161. shader := gl.CreateShader(shaderType)
  162. csources, free := gl.Strs(source)
  163. gl.ShaderSource(shader, 1, csources, nil)
  164. free()
  165. gl.CompileShader(shader)
  166. var status int32
  167. gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
  168. if status == gl.FALSE {
  169. var logLength int32
  170. gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)
  171. log := strings.Repeat("\x00", int(logLength+1))
  172. gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))
  173. return 0, fmt.Errorf("failed to compile %v: %v", source, log)
  174. }
  175. return shader, nil
  176. }

让我知道这篇文章对你有没有帮助,在 Twitter @kylewbanks 或者下方的连接,关注我以便获取最新的文章!


via: https:///blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board

作者:kylewbanks 译者:GitFtuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多