Linux中國

OpenGL 與 Go 教程(二)繪製遊戲面板

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

歡迎回到《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  - 右弦三角形

很簡單,對吧?現在讓我們用兩個這樣的三角形頂點做成正方形。把 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,
}

注意:你也要把在 maindraw 裡面命名的 triangle 改為 square

我們通過添加三個頂點,把頂點數增加了一倍,這三個頂點就是右上角的三角形,用來拼成方形。運行它看看效果:

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 的切片,這些細胞的數量與列數相等。如果我們把 rowscolumns 都設定成 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.50-0.5 這樣的點。如果點小於 0,我們就把它設置成原來的 2 倍(因為 OpenGL 坐標的範圍在 -11 之間,範圍大小是 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&apos;s Game of Life - 全部格子

這是你想看到的嗎?我們做的是在格子里為每一行每一列創建了一個方塊,然後給它上色,這就填滿了整個面板!

注釋掉 for 循環,我們就可以看到一個明顯獨立的細胞,像這樣:

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

cells[2][3].draw()

Conway&apos;s Game of Life - 一個單獨的細胞

這隻繪製坐標在 (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&apos;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 或者下方的連接,關注我以便獲取最新的文章!

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

作者:kylewbanks 譯者:GitFtuture 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國