此文写在golang游戏开发学习笔记-用golang画一个随时间变化颜色的正方形之后,感兴趣可以先去那篇文章了解一些基础知识,在这篇文章里,我们将创建一个非常简单(只有三个方块)但能自由探索的的3D世界
1.参考资料
learnOpenGl 的中文翻译,使用C++
实现的。
go-gl example go-gl的示例代码
2.基础概念
- 相关数学概念如矩阵,向量等,有兴趣的可以在网上找相关资料
- 纹理,可以理解为我们创建的模型的贴图
- 纹理坐标,取值范围在(0,0)到(1,1)之间,对应于纹理图像中的左下角和右上角
- 纹理环绕方式,纹理坐标超出范围之后做什么处理,比如重复纹理图像,填充别的颜色等等
- 纹理过滤,纹理的像素并不与坐标一一对应,需要我们指定映射方式
- 局部空间,可以理解为创建物体时会选定一个基准点,所有该物体的其他顶点都时相对于基准点而排布,这些顶点的坐标就叫局部坐标,构成的空间被称为局部空间
- 世界空间,这个比较好理解,可以理解为整个游戏的场景,创建物体之后需经过矩阵运算将物体移动到世界空间中
- 观察空间,顾名思义,从观察者的角度所看到的世界,同样是通过矩阵运算将世界空间坐标转化为观察者所看到的坐标
- 裁剪空间,受限于窗口大小,我们不可能看到整个世界空间,需要对世界空间进行裁剪,保留窗口所能显示的部分
所以我们需要三个矩阵,第一个负责将局部坐标(local
)转化为世界坐标(负责物体移动)命名为model
,第二个负责将世界坐标转化为我们从观察者角度所看到的坐标,命名为view
,第三个对我们所能看到的世界进行裁剪,显示在窗口上,命名为projection
,即,一个顶点坐标要经历如下变换
clip = projection * view * model * local
以上概念可以在learnOpenGl 中找到详细的概念解释,这里只做个概要
3.依赖
在C++
中opengl
有配套的矩阵运算包GLM(OpenGL Mathematics)
,然而作者并没能找到基于golang
的GLM
包,只找到了一个名为mgl
的包,仔细查看了一下源代码,需要的矩阵运算几乎都有,要注意的是,这个依赖包依赖于image
模块,而官方的image
模块被qiang了,所以最好是在gopath
目录里手动创建golang.org\x
目录,然后在github
镜像里直接下载依赖保存到目录中,在安装好image
依赖后运行
go get github.com/go-gl/mathgl/
4.实现
1.创建用于加载纹理的纹理类
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)
}
在构造函数中我们需要传入一个jpg格式的纹理文件路径(调用image/图片格式包下的Decode方法可以解析不同格式文件,这里为了简单,只解析jpg格式),和一个uint32类型的参数,这个参数指定在我们使用多个纹理时,着色器程序中不同纹理的占位符。在构造函数中我们指定了纹理放大缩小过滤方式和纹理环绕过滤方式
2.创建用于变换的矩阵
前面提到过我们要用到三个矩阵来对物体坐标进行变换
model = mgl32.Translate3D(0,0,1)
这个矩阵会将物体沿着z轴方向移动一个单位
接着调用mgl
的Perspective
方法创建一个透视矩阵,设置视野大小为45,近平面设为0.1,远平面设为100,只有在这两个平面之间的物体才会被渲染,可以理解为物体远离到了看不见的位置
projection := mgl32.Perspective(45, width/height, 0.1, 100.0)
最后我们使用mgl
的LookAt
方法创建一个视图矩阵,它用于将物体坐标转化为我们观察者的视角坐标,这个方法需要三个向量,第一个是观察者处于世界空间中的哪个位置,第二个是观察者要观察的方向,最后是表示观察者所观察方向的正上方的向量(可以通过右向量和观察方向向量叉乘得到)
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)
代码中position
代表观察者所处的位置(设为原点),front
代表观察者观察的方向(沿z轴的负方向),up
代表竖直方向。
至此,我们准备好了创建一个3D空间所需要的全部矩阵,之后的所有操作都转化为了对这三个矩阵的运算,要移动物体可以通过修改model矩阵实现,视角需要变化只需要修改view矩阵,视野缩放责可以通过修改projection矩阵来完成。
有个问题是,如何让我们观察的方向随着鼠标移动而变化?前面知道,front
代表观察者观察的方向,修改这个向量即可,这里要用到欧拉角,具体可以去网上找资料,我也不是很懂,这里直接复制了教程中的代码
最后为了进一步抽象相关操作,创建一个camera类
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.创建着色器
上一篇文章中我写过创建一个着色器的全部流程,这里我们将其封装为一个着色器类,可以直接从文件中构造并编译出着色器
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
}
两个着色器代码如下
#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.整合
我们在main
方法中对以上内容进行整合,包括按键输入处理,鼠标移动处理等
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)
}