为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Go

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


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


让我们现在开始做吧!


利用三角形绘制方形

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


triangle = []float32{

    -0.5, 0.5, 0,

    -0.5, -0.5, 0,

    0.5, -0.5, 0,

}

我们做的事情是,把最上面的顶点 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 结构体,用来表示格子的每一个单元,因此我们能够很灵活的选择绘制的数量:

type cell struct {

    drawable uint32

    x int

    y int

}

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


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


const (

    ...

    rows = 10

    columns = 10

)

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


func makeCells() [][]*cell {

    cells := make([][]*cell, rows, rows)

    for x := 0; x < rows; x++ {

        for y := 0; y < columns; y++ {

            c := newCell(x, y)

            cells[x] = append(cells[x], c)

        }

    }

    return cells

}

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


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


[

    [cell, cell],

    [cell, cell]

]

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


[

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],

    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell]

]

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


func newCell(x, y int) *cell {

    points := make([]float32, len(square), len(square))

    copy(points, square)

    for i := 0; i < len(points); i++ {

        var position float32

        var size float32

        switch i % 3 {

        case 0:

                size = 1.0 / float32(columns)

                position = float32(x) * size

        case 1:

                size = 1.0 / float32(rows)

                position = float32(y) * size

        default:

                continue

        }

        if points[i] < 0 {

                points[i] = (position * 2) - 1

        } else {

                points[i] = ((position + size) * 2) - 1

        }

    }

    return &cell{

        drawable: makeVao(points),

        x: x,

        y: y,

    }

}

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


接着,我们改变那些包含在 square 切片中定义的 0.5,0, -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。


func main() {

    ...

    // vao := makeVao(square)

    cells := makeCells()

    for !window.ShouldClose() {

        draw(cells, window, program)

    }

}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {

    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    gl.UseProgram(program)

    // TODO

    glfw.PollEvents()

    window.SwapBuffers()

}

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


func (c *cell) draw() {

    gl.BindVertexArray(c.drawable)

    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square) / 3))

}

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


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


func draw(cells [][]*cell, window *glfw.Window, program uint32) {

    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    gl.UseProgram(program)

    for x := range cells {

        for _, c := range cells[x] {

            c.draw()

        }

    }

    glfw.PollEvents()

    window.SwapBuffers()

}

如你所见,我们循环每一个细胞,调用它的 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. }

编译自:https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board作者: Kylewbanks

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消