代码仓库
https://github.com/phprao/go-graphic
变换
矩阵操作与向量操作:https://learnopengl-cn.github.io/01%20Getting%20started/07%20Transformations/
在OpenGL中,由于某些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是大部分的向量都是4分量的。
矩阵实际上就是一个数组
type Mat4 [16]float32
func Ident4() Mat4 {
return Mat4{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}
}
向量的缩放
向量的位移
齐次坐标(Homogeneous Coordinates)
齐次坐标
如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(译注:这也就是我们说的不能位移一个方向)。
有了位移矩阵我们就可以在3个方向x、y、z上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。
向量的旋转
沿任意轴旋转
建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会互相影响。
GLM是OpenGL Mathematics的缩写,GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。
github.com/go-gl/mathgl/mgl32
// 生成一个向量
v4 = mgl32.Vec4{1, 1, 1, 1}
v3 = mgl32.Vec3{3, 3, 3}
v4 = v3.Vec4(1)
v2 = v3.Vec2()
// 向量的Add, Sub, Mul, Dot, Cross, Len
// 矩阵之间点乘
A.Mul4(B)
// 生成4*4的单位矩阵
model := mgl32.Ident4()
// 生成一个沿向量(3,4,5)移动的变换矩阵trans3d
/*
[1, 0, 0, 3]
[0, 1, 0, 4]
[0, 0, 1, 5]
[0, 0, 0, 1]
*/
trans3d := mgl32.Translate3D(3,4,5)
// 使向量vec3(1,2,3)沿着向量(3,4,5)移动
// 结果 (4,6,8)
mgl32.TransformCoordinate(mgl32.Vec3{1, 2, 3}, trans3d)
// 生成缩放比例(2,2,2)的变换矩阵
// 如果缩放的比例是负值,会导致图像翻转
/*
[2, 0, 0, 0]
[0, 2, 0, 0]
[0, 0, 2, 0]
[0, 0, 0, 1]
*/
scale3d := mgl32.Scale3D(2, 2, 2)
// 使向量vec3(1,2,3)缩放(2,2,2)
// 结果 (2,4,6)
mgl32.TransformCoordinate(mgl32.Vec3{1, 2, 3}, scale3d)
// 沿轴(3,3,3)旋转20度的变化矩阵
/*
[5.735343 2.588426 8.066097 0.000000]
[8.066097 5.735343 2.588426 0.000000]
[2.588426 8.066097 5.735343 0.000000]
[0.000000 0.000000 0.000000 1.000000]
*/
rotate3d := mgl32.HomogRotate3D(mgl32.DegToRad(20), mgl32.Vec3{3, 3, 3})
// 使向量vec3(1,2,3)沿轴(3,3,3)旋转20度
// 结果 (35.11049, 27.302063, 35.92665)
mgl32.TransformCoordinate(mgl32.Vec3{1, 2, 3}, rotate3d)
math.Sin(angle)radiandegreemgl32.RadToDeg() 和 mgl32.DegToRad()
mgl32.Mat4
model := mgl32.Ident4()
modelUniform := gl.GetUniformLocation(program, gl.Str("model\x00"))
gl.UniformMatrix4fv(modelUniform, 1, false, &model[0])
uniform mat4 model;
示例:对现有纹理,实现先缩放,再旋转,再移动的效果
操作后效果
主要代码:
for !window.ShouldClose() {
......
gl.UseProgram(program)
rotate := mgl32.HomogRotate3D(mgl32.DegToRad(90), mgl32.Vec3{0, 0, 1})
scale := mgl32.Scale3D(0.5, 0.5, 0.5)
translate := mgl32.Translate3D(0.5, -0.5, 0)
// 顺序要反着看:依次是 scale,rotate,translate
transe := translate.Mul4(rotate).Mul4(scale)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
......
}
顶点着色器
......
uniform mat4 transe;
......
void main() {
gl_Position = transe * vec4(vPosition, 1.0);
......
}
我们可以让旋转的弧度随着时间变动,这样图像就旋转起来了
rotate := mgl32.HomogRotate3D(float32(glfw.GetTime()), mgl32.Vec3{0, 0, 1})
glfw.GetTime()
0.19938110100045076
0.3682488961570842
0.8834820281800326
...
1.0471016692995818
1.2141550765655154
1.380787958668221
...
表示窗口运行了多少秒。
示例2:在一个窗口中画两个箱子,一个在不停旋转,一个在不停缩小放大
for !window.ShouldClose() {
gl.ClearColor(0.2, 0.3, 0.3, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT)
gl.UseProgram(program)
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, texture1)
gl.Uniform1i(gl.GetUniformLocation(program, gl.Str("ourTexture1"+"\x00")), 0)
gl.ActiveTexture(gl.TEXTURE1)
gl.BindTexture(gl.TEXTURE_2D, texture2)
gl.Uniform1i(gl.GetUniformLocation(program, gl.Str("ourTexture2"+"\x00")), 1)
gl.BindVertexArray(vao)
// 第一个箱子
rotate := mgl32.HomogRotate3D(float32(glfw.GetTime()), mgl32.Vec3{0, 0, 1}) // 旋转效果
scale := mgl32.Scale3D(0.5, 0.5, 0.5)
translate := mgl32.Translate3D(0.5, -0.5, 0)
transe := translate.Mul4(rotate).Mul4(scale)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
gl.DrawElements(gl.TRIANGLES, pointNum, gl.UNSIGNED_INT, gl.Ptr(indices))
// 第二个箱子
rotate2 := mgl32.HomogRotate3D(mgl32.DegToRad(90), mgl32.Vec3{0, 0, 1})
s := float32(math.Sin(glfw.GetTime()))
scale2 := mgl32.Scale3D(s, s, s)
translate2 := mgl32.Translate3D(-0.5, 0.5, 0)
transe2 := translate2.Mul4(rotate2).Mul4(scale2)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe2[0])
gl.DrawElements(gl.TRIANGLES, pointNum, gl.UNSIGNED_INT, gl.Ptr(indices))
glfw.PollEvents()
window.SwapBuffers()
}
坐标系统
一个顶点在最终被转化为片段之前需要经历的所有不同状态。
- 局部空间(Local Space),或者称为物体空间(Object Space)
- 世界空间(World Space)
- 观察空间(View Space),或者称为视觉空间(Eye Space),摄像机空间(Camera Space)
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:
矩阵变换
- 从局部空间变换到世界空间:模型矩阵(Model Matrix)。
- 从世界空间变换到观察空间:观察矩阵(View Matrix)。
- 从观察空间变换到裁剪空间:投影矩阵(Projection Matrix)。
透视除法(Perspective Division)自动执行
正射投影矩阵(Orthographic Projection Matrix)透视投影矩阵(Perspective Projection Matrix)
正射投影
它由宽、高、近(Near)平面和远(Far)平面所指定。
// near平面为靠近观察者的平面
// 参数一二,表示near平面的左右坐标
// 参数三四,表示near平面的底顶坐标
// 参数五六,near和far平面距离屏幕的距离
mgl32.Ortho(0, 800, 0, 600, 0.1, 100)
正射投影对近处和远处的物体都一视同仁,也就说每个顶点的w分量都是1,但这与现实不符,实际上,同样大小的物体,距离人眼越远会看到的越小,这是由眼睛的构造决定的。
透视投影
// 第一个参数为视野角,通常为45度。
// 第二个参数为视口的宽高比。
// near和far平面距离屏幕的距离。通常设置near为0.1,far为100
mgl32.Perspective(mgl32.DegToRad(45.0), float32(windowWidth)/windowHeight, 0.1, 10.0)
距离摄像机越远,能看到的视野越大,但是屏幕大小是固定的,于是物体是会缩小的。
会修改w分量,离观察者越远的顶点坐标w分量越大。最后会让x,y,z都除以w分量,于是远处的物体就变小了。
视野角的特性:角度越小,那么看到的场景范围就越小,投影到屏幕上反而是放大的效果;反之,视野角越大,是缩小的效果。
最后的变换过程:
V_clip = M_projection * M_view * M_model * V_local
具体实践
Model Matrix
也就是说 Model 是用来调整单个的物体缩放 --> 旋转 --> 平移
View Matrix
也就是说 View 是用来调整整个场景的
Projection Matrix
将场景投影到窗口区域上,它决定了要将哪部分场景投影到屏幕上。
model := mgl32.HomogRotate3D(mgl32.DegToRad(-55), mgl32.Vec3{1, 0, 0})
view := mgl32.Translate3D(0, 0, -3)
projection := mgl32.Perspective(mgl32.DegToRad(45), float32(width)/float32(height), 0.1, 100)
transe := projection.Mul4(view).Mul4(model)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
如果有3D效果,那么就需要开启深度测试,它会对遮挡进行处理,否则效果比较奇怪。
gl.Enable(gl.DEPTH_TEST)
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
摄像机
前面将的坐标系统,是假设摄像机不动,而是场景在动,物体在动,其实还有一种观察方式就是让摄像机也动起来。
定义一个摄像机,就是创建一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。
定义一个摄像机的步骤:
cameraPos := mgl32.Vec3{0, 0, 3}
cameraTarget := mgl32.Vec3{0, 0, 0}
cameraDirction := cameraPos.Sub(cameraTarget)
up := mgl32.Vec3{0, 1, 0}
cameraRight := up.Cross(cameraDirction)
cameraUp := cameraDirction.Cross(cameraRight)
cameraPos, cameraTarget, upLookAt
camera := mgl32.LookAtV(cameraPos, cameraTarget, up)
projection * camera * model
示例1
一个立方体绕着Y轴旋转,正常情况下我们只能看到Y轴这个面,加入了相机之后我们就能看到一个立体效果。
model := mgl32.HomogRotate3D(float32(glfw.GetTime()), mgl32.Vec3{0, 1, 0})
camera := mgl32.LookAtV(mgl32.Vec3{2, 2, 2}, mgl32.Vec3{0, 0, 0}, mgl32.Vec3{0, 1, 0})
projection := mgl32.Perspective(mgl32.DegToRad(45), float32(width)/height, 0.1, 100)
transe := projection.Mul4(camera).Mul4(model)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
示例2
场景不动,摄像机的位置围绕着一个圆旋转,圆半径为3
radius := 3.0
cx := float32(math.Sin(glfw.GetTime()) * radius)
cz := float32(math.Cos(glfw.GetTime()) * radius)
camera := mgl32.LookAtV(mgl32.Vec3{cx, 2, cz}, mgl32.Vec3{0, 0, 0}, mgl32.Vec3{0, 1, 0})
projection := mgl32.Perspective(mgl32.DegToRad(45), float32(width)/height, 0.1, 100)
transe := projection.Mul4(camera)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
示例3
使用按键WSAD来控制相机左右前后移动,
cameraPos := mgl32.Vec3{0, 0, 3}
cameraFront := mgl32.Vec3{0, 0, -1}
cameraUp := mgl32.Vec3{0, 1, 0}
func KeyPressAction(window *glfw.Window) {
keyCallback := func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
cameraSpeed := float32(0.05)
if key == glfw.KeyW && action == glfw.Press {
cameraPos = cameraPos.Sub(cameraFront.Mul(cameraSpeed))
}
if key == glfw.KeyS && action == glfw.Press {
cameraPos = cameraPos.Add(cameraFront.Mul(cameraSpeed))
}
if key == glfw.KeyA && action == glfw.Press {
// Normalize 标准化坐标使其落在 [-1,1]
cameraPos = cameraPos.Add(cameraFront.Cross(cameraUp).Normalize().Mul(cameraSpeed))
}
if key == glfw.KeyD && action == glfw.Press {
cameraPos = cameraPos.Sub(cameraFront.Cross(cameraUp).Normalize().Mul(cameraSpeed))
}
// log.Println(cameraPos, cameraPos.Add(cameraFront))
}
window.SetKeyCallback(keyCallback)
}
func Run10() {
......
KeyPressAction(window)
for !window.ShouldClose() {
......
// 这样能保证无论我们怎么移动,摄像机都会注视着目标方向
camera := mgl32.LookAtV(cameraPos, cameraPos.Add(cameraFront), cameraUp)
projection := mgl32.Perspective(mgl32.DegToRad(45), float32(width)/height, 0.1, 100)
transe := projection.Mul4(camera)
gl.UniformMatrix4fv(gl.GetUniformLocation(program, gl.Str("transe\x00")), 1, false, &transe[0])
......
glfw.PollEvents()
window.SwapBuffers()
}
}
叉乘
这样能保证无论我们怎么移动,摄像机都会注视着目标方向keyCallbackcameraPoscameraFrontcameraTarget = cameraPos + cameraFront
视角移动
欧拉角
欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
俯仰角是描述我们如何往上或往下看的角,可以在第一张图中看到。第二张图展示了偏航角,偏航角表示我们往左和往右看的程度。滚转角代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。
最终的方向向量计算公式
// direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
鼠标控制
鼠标坐标系的原点为屏幕左上角,向右为X正,向下为Y正,因此Y轴的增量应该反过来。
刚进来的时候会出现抖动,那是因为默认的cursorX,cursorY在屏幕中心,而鼠标刚开始并不在屏幕中心,因此要初始化起始点。
俯仰角,要让用户不能看向高于89度的地方(在90度时视角会发生翻转),同样也不允许小于-89度。这样能够保证用户只能看到天空或脚下,但是不能超越这个限制。类比于人的眼睛仰视和俯视,超过了90度看到的东西会反过来。
偏航角却可以是360度旋转。
yawpitchcameraFrontcameraFront(0,0,-1)yawpitchcameraFront = (0,0,-1)pitch = 0, yaw = -90
var firstMouse bool
var cursorX float64 = 400
var cursorY float64 = 300
var yaw float64 = -90
var pitch float64
sensitivity := 0.05 // 鼠标移动的灵敏度
cursorPosCallback := func(w *glfw.Window, xpos float64, ypos float64) {
if firstMouse {
cursorX = xpos
cursorY = ypos
firstMouse = false
}
xoffset := sensitivity * (xpos - cursorX)
yoffset := sensitivity * (cursorY - ypos)
cursorX = xpos
cursorY = ypos
yaw += xoffset
pitch += yoffset
if pitch > 89 {
pitch = 89
}
if pitch < -89 {
pitch = -89
}
cameraFront = mgl32.Vec3{
float32(math.Cos(float64(mgl32.DegToRad(float32(pitch)))) * math.Cos(float64(mgl32.DegToRad(float32(yaw))))),
float32(math.Sin(float64(mgl32.DegToRad(float32(pitch))))),
float32(math.Cos(float64(mgl32.DegToRad(float32(pitch)))) * math.Sin(float64(mgl32.DegToRad(float32(yaw))))),
}.Normalize()
}
window.SetCursorPosCallback(cursorPosCallback)
天空盒
前面我们讲过,纹理贴图默认是双面贴的,也就是说立方体内部也贴好了,但是细细分析就会发现,外面和里面的图片是左右翻转的,如果此时我们把相机位置放到立方体中心会怎么样呢,其实这就是一个天空盒的效果,通过控制鼠标,我们可以在一个内部空间遨游。当然,更精细的天空盒需要六个面有不同的贴图。
当然,天空盒的正确操作应该是使用立方体贴图,也就是纹理坐标是三维的,参考实现 https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/06%20Cubemaps
首先是创建立方体纹理
func MakeTextureCube(filepathArray []string) uint32 {
var texture uint32
gl.GenTextures(1, &texture)
gl.BindTexture(gl.TEXTURE_CUBE_MAP, texture)
for i := 0; i < len(filepathArray); i++ {
imgFile2, _ := os.Open(filepathArray[i])
defer imgFile2.Close()
img2, _, _ := image.Decode(imgFile2)
rgba2 := image.NewRGBA(img2.Bounds())
draw.Draw(rgba2, rgba2.Bounds(), img2, image.Point{0, 0}, draw.Src)
// right, left, top, bottom, back, front
//
// TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515
// TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516
// TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517
// TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518
// TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519
// TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A
gl.TexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X+uint32(i), 0, gl.RGBA, int32(rgba2.Rect.Size().X), int32(rgba2.Rect.Size().Y), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba2.Pix))
}
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.TexParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
return texture
}
然后是顶点数据,我们不需要设置纹理坐标,而是直接取的顶点坐标作为纹理坐标。
#version 410
in vec3 vPosition;
out vec3 textureDir;
uniform mat4 transe;
void main() {
gl_Position = transe * vec4(vPosition, 1.0);
textureDir = vPosition;
}
samplerCube
#version 410
in vec3 textureDir;
out vec4 frag_colour;
uniform samplerCube cubemap;
void main() {
frag_colour = texture(cubemap, textureDir);
}
right, left, top, bottom, back, front
滚轮控制缩放
45.0f1.0f45.0f
var fov float64 = 45
scrollCallback := func(w *glfw.Window, xoff float64, yoff float64) {
if fov >= 1.0 && fov <= 45.0 {
fov -= yoff
}
if fov <= 1.0 {
fov = 1.0
}
if fov >= 45.0 {
fov = 45.0
}
}
window.SetScrollCallback(scrollCallback)
......
projection := mgl32.Perspective(mgl32.DegToRad(float32(fov)), float32(width)/height, 0.1, 100)
保存图片
ctrl+sgl.ReadPixels()func ReadPixels(x int32, y int32, width int32, height int32, format uint32, xtype uint32, pixels unsafe.Pointer)
gl.TexImage2D()
但是我们用的图形库基本都是将左上角作为(0,0)点的,因此保存的图片是Y轴上下颠倒的,因此需要自行翻转Y轴。
func (c *Camera) SavePng(filepath string) {
img := image.NewRGBA(image.Rect(0, 0, c.WindowWidth, c.WindowHeight))
gl.ReadPixels(0, 0, int32(c.WindowWidth), int32(c.WindowHeight), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix))
// 翻转Y坐标
for x := 0; x < c.WindowWidth; x++ {
for y := 0; y < c.WindowHeight/2; y++ {
s := img.RGBAAt(x, y)
t := img.RGBAAt(x, c.WindowHeight-1-y)
img.SetRGBA(x, y, t)
img.SetRGBA(x, c.WindowHeight-1-y, s)
}
}
if filepath == "" {
filepath = strconv.Itoa(int(time.Now().Unix())) + ".png"
}
f, _ := os.Create(filepath)
b := bufio.NewWriter(f)
png.Encode(b, img)
b.Flush()
f.Close()
}