TranslateProject/sources/tech/20170312 OpenGL Go Tutorial Part 2 Drawing the Game Board.md
2017-03-27 11:20:02 +08:00

17 KiB
Raw Blame History

OpenGL & Go Tutorial Part 2: Drawing the Game Board

Part 1: Hello, OpenGL  |  Part 2: Drawing the Game Board  |  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 havent gone through Part 1 youll 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 were not in the business of using triangles as our game unit so its time to turn the triangle into a square, and then well make an entire grid of them.

Lets get started!

Make a Square out of Triangles

Before we can make a square, lets 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 weve 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

Easy enough, right? Now lets make a square out of two of these. Lets 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: Youll also need to rename the two references to triangle to be square, namely in main and draw.

Here weve 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

Great, now we have the ability to draw a square! OpenGL isnt 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? Lets 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.

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

const (
    ...

    rows = 10
    columns = 10
)

Now lets 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 games board, and populate each element of the matrix with a cell using a new function called newCell which well write in just a moment.

Before moving on, lets take a moment to visualize what makeCells is creating. Were 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, wed create the following matrix:

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

Were creating a much larger matrix that10x10 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 were creating, lets 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,
    }
}

Theres quite a lot going on in this function so lets 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 cells 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 were at an X (i % 3 == 0) or Y (i % 3 == 1) coordinate of the shape (skipping Z since were operating in two dimensions) and determine the size (as a percentage of the entire game board) of the cell accordingly, as well as its 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.50 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 drawablefield 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. Well 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 well need each cell to know how to draw itself. Lets 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 cellvao 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

Is this what you expected? What weve 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

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.

Lets go ahead and remove the cells2.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, weve 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 well implement the core game logic and see some cool simulations!

Part 1: Hello, OpenGL  |  Part 2: Drawing the Game Board  |  Part 3: Implementing the Game

The full source code of the tutorial is available on GitHub.

Checkpoint

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

Let me know if this post was helpful on Twitter @kylewbanks or down below, and follow me to keep up with future posts!


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

作者:kylewbanks 译者:译者ID 校对:校对者ID

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