This text written in golang game development study notes - draw a square with golang change color over time after, interested can go to the article to know some basics In this article, we will create a very simple (only three cubes) but free to explore the world of 3D
1. References
learnOpenGl the Chinese translation, the use of C++
implementation.
go-gl example sample code of go-gl
2. Basic concepts
- Related mathematical concepts such as matrices , vectors , etc., are interested can find relevant information on the Internet
- Textures can be understood as the model we created maps
- Texture coordinates , in the range (0,0) to (1,1) between the texture image corresponding to the top right and bottom left
- Texture surround mode , the texture coordinates after processing beyond the scope of what to do, such as a duplicate image texture, color and the like other filler
- Texture filtering , texture pixel does not correspond with the coordinates, we need to specify the mapping mode
- Partial space , will be understood that a reference point is chosen to create the object, all other vertices are the object relative to the reference point are arranged, the coordinates of the vertices is called a local coordinate space configuration is referred to as the partial space
- World Space subject to matrix operations will object moves into world space after, this is better understood, can be understood as the scene of the game, create objects
- Viewing space , as the name implies, seen from the viewer's perspective of the world, the same matrix operation by the world space coordinates into coordinates seen by the viewer
- Clip space , limited by the size of the window, we could see the whole world of space, the need for world crop space, reserved part of the window can be displayed
So we need three matrices, the first one will be in charge of the local coordinate ( local
) into world coordinates (responsible for moving objects) named as model
second in charge of the world coordinates into the coordinates we have seen from the viewer's point of view, named view
, we can see a third of the world's crop, is displayed on the window, the name projection
, that is, a vertex coordinate transformation involves the following
clip = projection * view * model * local
The above concepts can be learnOpenGl find a detailed explanation of the concept, just to make a summary
3. Reliance
In C++
the opengl
supporting the matrix operations package GLM(OpenGL Mathematics)
, but did not able to find the author based on golang
the GLM
package, only to find a named mgl
package, a closer look at the source code, matrix operations required for almost all, we should note that this dependence package depends on the image
module, and the official image
module is qiang, so the best is gopath
to create a manual directory golang.org\x
directory and then github
downloaded directly dependent on image saved to the directory where the installed image
after running dependence
go get github.com/go-gl/mathgl/
4. Implement
1. Create a class for loading textures texture
package texture
import(
"os"
"image"
"image/jpeg"
"errors"
"github.com/go-gl/gl/v4.1-core/gl"
"image/draw"
)
type LocalTexture struct{
ID uint32
TEXTUREINDEX uint32
}
func NewLocalTexture(file string, TEXTUREINDEX uint32) *LocalTexture{
imgFile, err := os.Open(file)
if err != nil {
panic(err)
}
img, err := jpeg.Decode(imgFile)
if err != nil {
panic(err)
}
rgba := image.NewRGBA(img.Bounds())
if rgba.Stride != rgba.Rect.Size().X*4 {
panic(errors.New("unsupported stride"))
}
draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)
var textureID uint32
gl.GenTextures(1, &textureID)
gl.ActiveTexture(TEXTUREINDEX)
gl.BindTexture(gl.TEXTURE_2D, textureID)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)
gl.TexImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
int32(rgba.Rect.Size().X),
int32(rgba.Rect.Size().Y),
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
gl.Ptr(rgba.Pix))
return &LocalTexture{ ID: textureID,TEXTUREINDEX:TEXTUREINDEX}
}
func (texture *LocalTexture) Use(){
gl.ActiveTexture(texture.TEXTUREINDEX)
gl.BindTexture(gl.TEXTUR_2D, texture.ID)
}
In the constructor we need to pass a file path texture jpg format (call Decode method in the image / picture package format can parse different file formats, here for simplicity, only the analytical jpg format), and a parameter of type uint32, this when we use the parameter specifies a plurality of textures, the texture shader program different placeholder. In the constructor we specify texture filtering and texture zoom mode around the filter
2. Create a transformation matrix
I mentioned earlier we have to use three matrix transforming the object coordinates
model = mgl32.Translate3D(0,0,1)
This matrix will move the object along the z-axis direction, a unit
then invokes mgl
the Perspective
method to create a perspective matrix, field size set to 45, the near plane is 0.1, the far plane is set to 100, only the object between the two planes will be rendered, it can be understood as objects away from the invisible to the position
projection := mgl32.Perspective(45, width/height, 0.1, 100.0)
Finally, we use mgl
the LookAt
method to create a view matrix, which is used to coordinate into an object coordinate our perspective of the observer, this method requires three vectors, the first of which is the observer position in world space, and the second is direction of the observer to observe, directly above the last is a vector representing the viewing direction of a viewer (through the right fork vector can be a vector and the observation direction obtained by)
position := mgl32.Vec3{0, 0, 0}
front := mgl32.Vec3{0, 0, -1}
up:= mgl32.Vec3{0, 1, 0}
target := position.Add(front)
view := mgl32.LookAtV(position,target, up)
Code position
representative of the position of the observer is located (as the origin), front
the representative direction (along the z-axis negative direction) viewed by an observer, up
on behalf of a vertical direction.
At this point, we are ready to create a 3D matrix of all the space needed, after all operations are converted to these three matrix operations, to a moving object can be achieved by modifying the model matrix, perspective needs to be changed only need to modify the view matrix, zoom responsibility vision can be accomplished by modifying the projection matrix.
One problem is how to make the direction we observe with the mouse to move and change? Front know, front
on behalf of the observer observed, modifications to this vector are used here to Euler angles, can go online to find specific information, I do not really understand, directly copy the code tutorial here
last To further abstract the relevant operation, creating a camera class
package camera
import(
"github.com/go-gl/mathgl/mgl64"
"github.com/go-gl/mathgl/mgl32"
"math"
)
type Direction int
const (
FORWARD Direction = 0 // 摄像机移动状态:前
BACKWARD Direction = 1 // 后
LEFT Direction = 2 // 左
RIGHT Direction = 3 // 右
)
type LocalCamera struct{
position mgl32.Vec3
front mgl32.Vec3
up mgl32.Vec3
right mgl32.Vec3
wordUp mgl32.Vec3
yaw float64
pitch float64
zoom float32
movementSpeed float32
mouseSensitivity float32
constrainPitch bool
}
func NewDefaultCamera() *LocalCamera{
position := mgl32.Vec3{0, 0, 0}
front := mgl32.Vec3{0, 0, -1}
wordUp := mgl32.Vec3{0, 1, 0}
yaw := float64(-90)
pitch := float64(0)
movementSpeed := float32(2.5)
mouseSensitivity := float32(0.1)
zoom := float32(45)
constrainPitch := true
localCamera := &LocalCamera{position:position,
front:front,
wordUp:wordUp,
yaw:yaw,
pitch:pitch,
movementSpeed:movementSpeed,
mouseSensitivity:mouseSensitivity,
zoom:zoom,
constrainPitch:constrainPitch}
localCamera.updateCameraVectors()
return localCamera
}
//获取当前透视矩阵
func (localCamera *LocalCamera) GetProjection(width float32, height float32) *float32{
projection := mgl32.Perspective(mgl32.DegToRad(localCamera.zoom), float32(width)/height, 0.1, 100.0)
return &projection[0]
}
//鼠标移动回调
func (localCamera *LocalCamera) ProcessMouseMovement(xoffset float32, yoffset float32){
xoffset *= localCamera.mouseSensitivity
yoffset *= localCamera.mouseSensitivity
localCamera.yaw += float64(xoffset)
localCamera.pitch += float64(yoffset)
// Make sure that when pitch is out of bounds, screen doesn't get flipped
if (localCamera.constrainPitch){
if (localCamera.pitch > 89.0){
localCamera.pitch = 89.0
}
if (localCamera.pitch < -89.0){
localCamera.pitch = -89.0
}
}
localCamera.updateCameraVectors();
}
//鼠标滑动回调
func (localCamera *LocalCamera) ProcessMouseScroll(yoffset float32){
if (localCamera.zoom >= 1.0 && localCamera.zoom <= 45.0){
localCamera.zoom -= yoffset;
}
if (localCamera.zoom <= 1.0){
localCamera.zoom = 1.0;
}
if (localCamera.zoom >= 45.0){
localCamera.zoom = 45.0;
}
}
//键盘回调
func (localCamera *LocalCamera) ProcessKeyboard(direction Direction, deltaTime float32){
velocity := localCamera.movementSpeed * deltaTime;
if (direction == FORWARD){
localCamera.position = localCamera.position.Add(localCamera.front.Mul(velocity))
}
if (direction == BACKWARD){
localCamera.position = localCamera.position.Sub(localCamera.front.Mul(velocity))
}
if (direction == LEFT){
localCamera.position = localCamera.position.Sub(localCamera.right.Mul(velocity))
}
if (direction == RIGHT){
localCamera.position = localCamera.position.Add(localCamera.right.Mul(velocity))
}
}
//获取view
func (localCamera *LocalCamera) GetViewMatrix() *float32{
target := localCamera.position.Add(localCamera.front)
view := mgl32.LookAtV(localCamera.position,target, localCamera.up)
return &view[0]
}
//更新view
func (localCamera *LocalCamera) updateCameraVectors(){
x := math.Cos(mgl64.DegToRad(localCamera.yaw)) * math.Cos(mgl64.DegToRad(localCamera.pitch))
y := math.Sin(mgl64.DegToRad(localCamera.pitch))
z := math.Sin(mgl64.DegToRad(localCamera.yaw)) * math.Cos(mgl64.DegToRad(localCamera.pitch));
localCamera.front = mgl32.Vec3{float32(x),float32(y),float32(z)}
localCamera.right = localCamera.front.Cross(localCamera.wordUp).Normalize()
localCamera.up = localCamera.right.Cross(localCamera.front).Normalize()
}
3. Create Shader
The last article I wrote a shader to create all the processes, where we will be packaged as a shader class can be constructed directly from the file and compile a shader
package shader
import (
"io/ioutil"
"fmt"
"github.com/go-gl/gl/v4.1-core/gl"
"strings"
)
type LocalShader struct{
ID uint32
}
func (shader *LocalShader) Use(){
gl.UseProgram(shader.ID)
}
func (shader *LocalShader) SetBool(name string, value bool){
var a int32 = 0;
if(value){
a = 1
}
gl.Uniform1i(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), a)
}
func (shader *LocalShader) SetInt(name string, value int32){
gl.Uniform1i(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), value)
}
func (shader *LocalShader) SetFloat(name string, value float32){
gl.Uniform1f(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), value)
}
func (shader *LocalShader) SetMatrix4fv(name string, value *float32){
gl.UniformMatrix4fv(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), 1,false,value)
}
func NewLocalShader(vertexPath string, fragmentPath string) *LocalShader{
vertexString, err := ioutil.ReadFile(vertexPath)
if err != nil{
panic(err)
}
fragmentString, err := ioutil.ReadFile(fragmentPath)
if err != nil{
panic(err)
}
return NewStringShader(string(vertexString),string(fragmentString))
}
func NewStringShader(vertexString string, fragmentString string) *LocalShader{
vertexShader,err := compileShader(vertexString+"\x00", gl.VERTEX_SHADER)
if err != nil{
panic(err)
}
fragmentShader,err := compileShader(fragmentString+"\x00", gl.FRAGMENT_SHADER)
if err != nil{
panic(err)
}
progID := gl.CreateProgram()
gl.AttachShader(progID, vertexShader)
gl.AttachShader(progID, fragmentShader)
gl.LinkProgram(progID)
gl.DeleteShader(vertexShader)
gl.DeleteShader(fragmentShader)
return &LocalShader{ ID: progID}
}
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
}
Two shader code as follows
#version 410 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main(){
gl_Position = projection * view * model * vec4(aPos,1.0);
TexCoord = aTexCoord;
}
#version 410 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.5);
}
4. Integration
We main
carried out the integration of the above method, the process comprising key input, mouse movement processing
package main
import(
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/go-gl/gl/v4.1-core/gl"
"log"
"legend/shader"
"runtime"
"legend/texture"
"legend/camera"
"github.com/go-gl/mathgl/mgl32"
)
const (
width = 800
height = 600
)
var (
vertices = []float32 {
-0.5, -0.5, -0.5, 0.0, 0.0,
0.5, -0.5, -0.5, 1.0, 0.0,
0.5, 0.5, -0.5, 1.0, 1.0,
0.5, 0.5, -0.5, 1.0, 1.0,
-0.5, 0.5, -0.5, 0.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 0.0,
-0.5, -0.5, 0.5, 0.0, 0.0,
0.5, -0.5, 0.5, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0, 1.0,
-0.5, 0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 1.0, 0.0,
-0.5, 0.5, -0.5, 1.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 1.0,
-0.5, -0.5, -0.5, 0.0, 1.0,
-0.5, -0.5, 0.5, 0.0, 0.0,
-0.5, 0.5, 0.5, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 0.0,
0.5, 0.5, -0.5, 1.0, 1.0,
0.5, -0.5, -0.5, 0.0, 1.0,
0.5, -0.5, -0.5, 0.0, 1.0,
0.5, -0.5, 0.5, 0.0, 0.0,
0.5, 0.5, 0.5, 1.0, 0.0,
-0.5, -0.5, -0.5, 0.0, 1.0,
0.5, -0.5, -0.5, 1.0, 1.0,
0.5, -0.5, 0.5, 1.0, 0.0,
0.5, -0.5, 0.5, 1.0, 0.0,
-0.5, -0.5, 0.5, 0.0, 0.0,
-0.5, -0.5, -0.5, 0.0, 1.0,
-0.5, 0.5, -0.5, 0.0, 1.0,
0.5, 0.5, -0.5, 1.0, 1.0,
0.5, 0.5, 0.5, 1.0, 0.0,
0.5, 0.5, 0.5, 1.0, 0.0,
-0.5, 0.5, 0.5, 0.0, 0.0,
-0.5, 0.5, -0.5, 0.0, 1.0,
};
position = []mgl32.Mat3{
mgl32.Mat3{0,0,0},
mgl32.Mat3{2,5,-15},
mgl32.Mat3{-1.5,-2.2,-2.5},
}
deltaTime = float32(0.0); // time between current frame and last frame
lastFrame = float32(0.0);
acamera = camera.NewDefaultCamera()
firstMouse = true
lastX = width / 2.0
lastY = height / 2.0
)
func main() {
runtime.LockOSThread()
window := initGlfw()
defer glfw.Terminate()
initOpenGL()
vao,vbo := makeVao(vertices,nil)
shader := shader.NewLocalShader("./shader/shader-file/shader.vs","./shader/shader-file/shader.fs")
shader.Use()
shader.SetInt("texture1", 0)
shader.SetInt("texture2", 1)
texture1 := texture.NewLocalTexture("./texture/texture-file/face.jpg",gl.TEXTURE0)
texture2 := texture.NewLocalTexture("./texture/texture-file/wood.jpg",gl.TEXTURE1)
texture1.Use()
texture2.Use()
projection := acamera.GetProjection(width,height)
shader.SetMatrix4fv("projection", projection)
for !window.ShouldClose() {
currentFrame := float32(glfw.GetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
clear()
texture1.Use()
texture2.Use()
view := acamera.GetViewMatrix()
shader.SetMatrix4fv("view",view)
for _, v := range position {
model := mgl32.HomogRotate3DX(float32(glfw.GetTime())).Mul4(mgl32.HomogRotate3DY(float32(glfw.GetTime())))
model = mgl32.Translate3D(v[0],v[1],v[2]).Mul4(model)
shader.SetMatrix4fv("model",&model[0])
draw(vao)
}
processInput(window)
glfw.PollEvents()
window.SwapBuffers()
}
gl.DeleteVertexArrays(1, &vao);
gl.DeleteBuffers(1, &vbo);
glfw.Terminate()
}
func initGlfw() *glfw.Window {
if err := glfw.Init(); err != nil {
panic(err)
}
glfw.WindowHint(glfw.Resizable, glfw.False)
window, err := glfw.CreateWindow(width, height, "test", nil, nil)
window.SetCursorPosCallback(mouse_callback)
if err != nil {
panic(err)
}
window.MakeContextCurrent()
window.SetInputMode(glfw.CursorMode,glfw.CursorDisabled)
return window
}
func initOpenGL(){
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
log.Println("OpenGL version", version)
gl.Enable(gl.DEPTH_TEST)
}
func makeVao(points []float32,indices []uint32) (uint32,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.VertexAttribPointer(0, 3, gl.FLOAT, false, 5 * 4, gl.PtrOffset(0))
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointer(1, 2, gl.FLOAT, false, 5 * 4, gl.PtrOffset(3 * 4))
gl.EnableVertexAttribArray(1)
if(indices != nil){
var ebo uint32
gl.GenBuffers(2,&ebo)
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER,ebo)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER,4*len(indices),gl.Ptr(indices),gl.STATIC_DRAW)
}
return vao,vbo
}
func processInput(window *glfw.Window){
if(window.GetKey(glfw.KeyW) == glfw.Press){
acamera.ProcessKeyboard(camera.FORWARD,deltaTime)
}
if(window.GetKey(glfw.KeyS) == glfw.Press){
acamera.ProcessKeyboard(camera.BACKWARD,deltaTime)
}
if(window.GetKey(glfw.KeyA) == glfw.Press){
acamera.ProcessKeyboard(camera.LEFT,deltaTime)
}
if(window.GetKey(glfw.KeyD) == glfw.Press){
acamera.ProcessKeyboard(camera.RIGHT,deltaTime)
}
if(window.GetKey(glfw.KeyEscape) == glfw.Press){
window.SetShouldClose(true)
}
}
func mouse_callback(window *glfw.Window, xpos float64, ypos float64){
if(firstMouse){
lastX = xpos
lastY = ypos
firstMouse = false
}
xoffset := float32(xpos - lastX)
yoffset := float32(lastY - ypos)
lastX = xpos
lastY = ypos
acamera.ProcessMouseMovement(xoffset, yoffset)
}
func draw(vao uint32) {
gl.BindVertexArray(vao)
gl.DrawArrays(gl.TRIANGLES,0,36)
}
func clear(){
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
}