OpenGL 与 Go 教程（二）绘制游戏面板
OpenGL Golang
![](/data/attachment/album/201710/07/223453r5tqot1qsp0s6ga6.jpg)


* [第一节: Hello, OpenGL](/article-8933-1.html)
* [第二节: 绘制游戏面板](/article-8937-1.html)
* [第三节：实现游戏功能](/article-8969-1.html)


这篇教程的所有源代码都可以在 [GitHub](https://github.com/KyleBanks/conways-gol) 上找到。


欢迎回到《OpenGL 与 Go 教程》。如果你还没有看过[第一节](/article-8933-1.html)，那就要回过头去看看那一节。


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


让我们现在开始做吧！


### 利用三角形绘制方形


在我们绘制方形之前，先把三角形变成直角三角形。打开 `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  - 右弦三角形](/data/attachment/album/201710/07/223511c84rhv985frva4vv.png)


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



```
square = []float32{
    -0.5, 0.5, 0,
    -0.5, -0.5, 0,
    0.5, -0.5, 0,

    -0.5, 0.5, 0,
    0.5, 0.5, 0,
    0.5, -0.5, 0,
}

```

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


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


![Conway's Game of Life - 两个三角形构成方形](/data/attachment/album/201710/07/223511wghwhbgk6j1csvvh.png)


很好，现在我们能够绘制正方形了！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
}

```

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


在接着往下阅读前，我们先花一点时间来看看 `makeCells` 函数做了些什么。我们创造了一个切片，这个切片的长度和格子的行数相等，每一个切片里面都有一个<ruby> 细胞 <rt>  cell </rt></ruby>的切片，这些细胞的数量与列数相等。如果我们把 `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 - 全部格子](/data/attachment/album/201710/07/223512fukkduvyxvxvj5d5.png)


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


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



```
// for x := range cells {
//     for _, c := range cells[x] {
//         c.draw()
//     }
// }

cells[2][3].draw()

```

![Conway's Game of Life - 一个单独的细胞](/data/attachment/album/201710/07/223512yk5w3el05emzahzi.png)


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


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


### 总结


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


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


### 回顾


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



```
package main

import (
    "fmt"
    "log"
    "runtime"
    "strings"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

const (
    width  = 500
    height = 500

    vertexShaderSource = `
        #version 410
        in vec3 vp;
        void main() {
            gl_Position = vec4(vp, 1.0);
        }
    ` + "\x00"

    fragmentShaderSource = `
        #version 410
        out vec4 frag_colour;
        void main() {
            frag_colour = vec4(1, 1, 1, 1.0);
        }
    ` + "\x00"

    rows    = 10
    columns = 10
)

var (
    square = []float32{
        -0.5, 0.5, 0,
        -0.5, -0.5, 0,
        0.5, -0.5, 0,

        -0.5, 0.5, 0,
        0.5, 0.5, 0,
        0.5, -0.5, 0,
    }
)

type cell struct {
    drawable uint32

    x int
    y int
}

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()
    program := initOpenGL()

    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)

    for x := range cells {
        for _, c := range cells[x] {
            c.draw()
        }
    }

    glfw.PollEvents()
    window.SwapBuffers()
}

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
}

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,
    }
}

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

// 初始化 glfw，返回一个可用的 Window
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4)
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
        panic(err)
    }
    window.MakeContextCurrent()

    return window
}

// 初始化 OpenGL 并返回一个可用的着色器程序
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }

    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

// 初始化并返回由 points 提供的顶点数组
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.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

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
}

```

让我知道这篇文章对你有没有帮助，在 Twitter [@kylewbanks](https://twitter.com/kylewbanks) 或者下方的连接，关注我以便获取最新的文章！




---


via: <https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board>


作者：[kylewbanks](https://twitter.com/kylewbanks) 译者：[GitFtuture](https://github.com/GitFuture) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
# OpenGL & Go Tutorial Part 2: Drawing the Game Board

[@kylewbanks](https://twitter.com/kylewbanks)on Mar 12, 2017.

*Hey, if you didn't already know, I'm currently working on an open world stealth exploration game called*

[Farewell North](https://farewell-north.com)in my spare time, which is available to wishlist on Steam!* Part 1: Hello, OpenGL* |

*|*

[Part 2: Drawing the Game Board](/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board)

[Part 3: Implementing the Game](/blog/tutorial-opengl-with-golang-part-3-implementing-the-game)*The full source code of the tutorial is available on GitHub.*

Welcome back to the *OpenGL & Go Tutorial!* If you haven’t gone through [Part 1](/blog/tutorial-opengl-with-golang-part-1-hello-opengl) you’ll definitely want to take a step back and check it out.

At this point you should be the proud creator of a magnificent white triangle, but we’re not in the business of using triangles as our game unit so it’s time to turn the triangle into a square, and then we’ll make an entire grid of them.

Let’s get started!

## Make a Square out of Triangles

Before we can make a square, let’s turn our triangle into a right-angle. Open up **main.go** and change the **triangle** definition to look like so:

triangle = []float32{ -0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0, }

What we’ve done is move the X-coordinate of the top vertex to the left (**-0.5**), giving us a triangle like so:

![Conway's Game of Life in OpenGL and Golang Tutorial - Right-Angle Triangle](/images/post/golang-opengl-conway-4.png)

Easy enough, right? Now let’s make a square out of two of these. Let’s rename **triangle** to **square** and add a second, inverted right-angle triangle to the slice:

square = []float32{ -0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0, -0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0, }

Note: You’ll also need to rename the two references to **triangle** to be **square**, namely in **main** and **draw**.

Here we’ve doubled the number of points by adding a second set of three vertices to be our upper top-right triangle to complete the square. Run it for glory:

![Conway's Game of Life in OpenGL and Golang Tutorial - Two Triangles Make a Square](/images/post/golang-opengl-conway-5.png)

Great, now we have the ability to draw a square! OpenGL isn’t so tough after all, is it?

## Draw a Grid of Squares covering the Window

Now that we can draw one square, how about 100 of them? Let’s create a **cell** struct to represent each unit of our grid so that we can be flexible in the number of squares we draw:

type cell struct { drawable uint32 x int y int }

The **cell** contains a **drawable** which is a square **Vertex Array Object** just like the one we created above, and an X and Y coordinate to dictate where on the grid this cell resides.

We’re also going to want two more constants that define the size and shape of our grid:

const ( ... rows = 10 columns = 10 )

Now let’s add a function to create the grid:

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 }

Here we create a multi-dimensional slice to represent our game’s board, and populate each element of the matrix with a **cell** using a new function called **newCell** which we’ll write in just a moment.

Before moving on, let’s take a moment to visualize what **makeCells** is creating. We’re creating a slice that is equal in length to the number of rows on the grid, and each of these slices contains a slice of cells, equal in length to the number of columns. If we were to define **rows** and **columns** each equal to two, we’d create the following matrix:

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


We’re creating a much larger matrix that’s **10x10** cells:

```
[
[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]
]
```


Now that the we understand the shape and representation of the matrix we’re creating, let’s have a look at **newCell** which we use to actually populate the matrix:

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, } }

There’s quite a lot going on in this function so let’s break it down. The first thing we do is create a copy of our **square** definition. This allows us to change its contents to customize the current cell’s position, without impacting any other cells that are also using the **square** slice. Next we iterate over the **points** copy and act based on the current index. We use a modulo operation to determine if we’re at an X (**i % 3 == 0**) or Y (**i % 3 == 1**) coordinate **of the shape** (skipping Z since we’re operating in two dimensions) and determine the size (as a percentage of the entire game board) of the cell accordingly, as well as it’s position based on the X and Y coordinate of the cell **on the game board**.

Next, we modify the points which currently contain a combination of **0.5**, **0** and **-0.5** as we defined them in the **square** slice. If the point is less than zero, we set it equal to the position times 2 (because OpenGL coordinates have a range of 2, between **-1** and **1**), minus 1 to normalize to OpenGL coordinates. If the position is greater than or equal to zero, we do the same thing but add the size we calculated.

The purpose of this is to set the scale of each cell so that it fills only its percentage of the game board. Since we have 10 rows and 10 columns, each cell will be given 10% of the width and 10% of the height of the game board.

Finally, after all the points have been scaled and positioned, we create a **cell** with the X and Y coordinate provided, and set the **drawable** field equal to a **Vertex Array Object** created from the **points** slice we just manipulated.

Alright, now in **main** we can remove our call to **makeVao** and replace it with a call to **makeCells**. We’ll also change **draw** to take the matrix of cells instead of a single **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() }

Now we’ll need each cell to know how to draw itself. Let’s add a **draw** function to the **cell**:

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

This should look familiar, its nearly identical to how we were drawing the square **vao** in **draw** previously, the only difference being we **BindVertexArray** using **c.drawable**, which is the cell’s **vao** we created in **newCell**.

Back in the main **draw** function, we can loop over each cell and have it draw itself:

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() }

As you can see we loop over each of the cells and call its **draw** function. If you run the application you should see the following:

![Conway's Game of Life in OpenGL and Golang Tutorial - Full Grid](/images/post/golang-opengl-conway-6.png)

Is this what you expected? What we’ve done is create a square for each row and column on the grid, and colored it in, effectively filling the entire game board!

We can see an visualize individual cells by commenting out the for-loop for a moment and doing the following:

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

![Conway's Game of Life in OpenGL and Golang Tutorial - A Single Cell](/images/post/golang-opengl-conway-7.png)

This draws only the cell located at coordinate **(X=2, Y=3)**. As you can see, each individual cell takes up a small portion of the game board, and is responsible for drawing its own space. We can also see that our game board has its origin, that is the **(X=0, Y=0)** coordinate, in the bottom-left corner of the window. This is simply a result of the way our **newCell** function calculates the position, and could be made to use the top-right, bottom-right, top-left, center, or any other position as its origin.

Let’s go ahead and remove the **cells[2][3].draw()** line and uncomment the for-loop, leaving us with the fully drawn grid we had above.

## Summary

Alright - we can now use two triangles to draw a square, and we have ourselves a game board! We should be proud, we’ve covered a lot of ground up to this point and to be completely honest, the hardest part is behind us now!

Next up in [Part 3](/blog/tutorial-opengl-with-golang-part-3-implementing-the-game) we’ll implement the core game logic and see some cool simulations!

* Part 1: Hello, OpenGL* |

*|*

[Part 2: Drawing the Game Board](/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board)

[Part 3: Implementing the Game](/blog/tutorial-opengl-with-golang-part-3-implementing-the-game)*The full source code of the tutorial is available on GitHub.*

## Checkpoint

Here’s the contents of **main.go** at this point of the tutorial:

package main import ( "fmt" "log" "runtime" "strings" "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl "github.com/go-gl/glfw/v3.2/glfw" ) const ( width = 500 height = 500 vertexShaderSource = ` #version 410 in vec3 vp; void main() { gl_Position = vec4(vp, 1.0); } ` + "\x00" fragmentShaderSource = ` #version 410 out vec4 frag_colour; void main() { frag_colour = vec4(1, 1, 1, 1.0); } ` + "\x00" rows = 10 columns = 10 ) var ( square = []float32{ -0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0, -0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, -0.5, 0, } ) type cell struct { drawable uint32 x int y int } func main() { runtime.LockOSThread() window := initGlfw() defer glfw.Terminate() program := initOpenGL() 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) for x := range cells { for _, c := range cells[x] { c.draw() } } glfw.PollEvents() window.SwapBuffers() } 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 } 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, } } func (c *cell) draw() { gl.BindVertexArray(c.drawable) gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3)) } // initGlfw initializes glfw and returns a Window to use. func initGlfw() *glfw.Window { if err := glfw.Init(); err != nil { panic(err) } glfw.WindowHint(glfw.Resizable, glfw.False) glfw.WindowHint(glfw.ContextVersionMajor, 4) glfw.WindowHint(glfw.ContextVersionMinor, 1) glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil) if err != nil { panic(err) } window.MakeContextCurrent() return window } // initOpenGL initializes OpenGL and returns an intiialized program. func initOpenGL() uint32 { if err := gl.Init(); err != nil { panic(err) } version := gl.GoStr(gl.GetString(gl.VERSION)) log.Println("OpenGL version", version) vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER) if err != nil { panic(err) } fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER) if err != nil { panic(err) } prog := gl.CreateProgram() gl.AttachShader(prog, vertexShader) gl.AttachShader(prog, fragmentShader) gl.LinkProgram(prog) return prog } // makeVao initializes and returns a vertex array from the points provided. 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.BindBuffer(gl.ARRAY_BUFFER, vbo) gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil) return vao } 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 }

[@kylewbanks](https://twitter.com/kylewbanks)or down below!