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
),這就變成了像這樣的三角形:
很簡單,對吧?現在讓我們用兩個這樣的三角形頂點做成正方形。把 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
。
我們通過添加三個頂點,把頂點數增加了一倍,這三個頂點就是右上角的三角形,用來拼成方形。運行它看看效果:
很好,現在我們能夠繪製正方形了!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
}
這裡我們創建多維的 切片 ,代表我們的遊戲面板,用名為 newCell
的新函數創建的 cell
來填充矩陣的每個元素,我們待會就來實現 newCell
這個函數。
在接著往下閱讀前,我們先花一點時間來看看 makeCells
函數做了些什麼。我們創造了一個切片,這個切片的長度和格子的行數相等,每一個切片裡面都有一個 細胞 的切片,這些細胞的數量與列數相等。如果我們把 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
函數。如果運行這段代碼,你能看到像下面這樣的東西:
這是你想看到的嗎?我們做的是在格子里為每一行每一列創建了一個方塊,然後給它上色,這就填滿了整個面板!
注釋掉 for 循環,我們就可以看到一個明顯獨立的細胞,像這樣:
// for x := range cells {
// for _, c := range cells[x] {
// c.draw()
// }
// }
cells[2][3].draw()
這隻繪製坐標在 (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 或者下方的連接,關注我以便獲取最新的文章!
via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board
作者:kylewbanks 譯者:GitFtuture 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive