golang,OpenGL,计算机图形学(二)

news2024/9/21 14:43:38

代码仓库

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)

向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y、z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的)。

如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(译注:这也就是我们说的不能位移一个方向)。

有了位移矩阵我们就可以在3个方向x、y、z上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。

向量的旋转

在这里插入图片描述

沿任意轴旋转

在这里插入图片描述

建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会互相影响。

GLM是OpenGL Mathematics的缩写,GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。

对应golang中的包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)接受的是弧度radian,而不是度数degree,所以此处也要传弧度值,转换函数mgl32.RadToDeg() 和 mgl32.DegToRad()

变换矩阵类型为mgl32.Mat4类型,我们使用uniform将其传入到着色器中

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)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(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 是用来调整整个场景的。OpenGL是一个右手坐标系,所以我们需要沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现。它会给我们一种我们在往后移动的感觉。想象一下,一个立体的场景在那里不动,人可以选择从不同的点位来观察它,摄像机不能旋转,它永远是垂直于屏幕在看。当然,我们也可以让摄像机转到起来,这是另外一个知识点。

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)

摄像机

前面将的坐标系统,是假设摄像机不动,而是场景在动,物体在动,其实还有一种观察方式就是让摄像机也动起来。

定义一个摄像机,就是创建一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

在这里插入图片描述

定义一个摄像机的步骤:

  • 摄像机的位置:世界空间中的一个点,代表摄像机放在这里,位置越远看到的物体越小,P1(x,y,z)

    cameraPos := mgl32.Vec3{0, 0, 3}
    
  • 摄像机的方向:在世界空间中,摄像机指向哪里,因为它代表的只是一个方向,因此在该方向上选取一点即可,比如摄像机指向原点P2(0,0,0),那么摄像机的方向就是 P1-P2,就是图中蓝色的箭头,但是这个方向却和摄像机拍照的方向相反。

    cameraTarget := mgl32.Vec3{0, 0, 0}
    cameraDirction := cameraPos.Sub(cameraTarget)
    
  • 右轴:也叫X轴正方形,为获取右向量我们需要先使用一个小技巧:先定义一个上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量叉乘的顺序就会得到相反的指向x轴负方向的向量)。

    up := mgl32.Vec3{0, 1, 0}
    cameraRight := up.Cross(cameraDirction)
    
  • 上轴:现在我们已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘。

    cameraUp := cameraDirction.Cross(cameraRight)
    

于是我们发现,只要给定了cameraPos, cameraTarget, up三个向量,我们就可以构造出一个摄像机坐标,于是就有了LookAt函数。

camera := mgl32.LookAtV(cameraPos, cameraTarget, up)

在使用的过程中,摄像机其实就是view,因此顺序是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()
	}
}

A和D需要正交移动,因此要用向量的叉乘

上面注释中有些这样能保证无论我们怎么移动,摄像机都会注视着目标方向,该如何理解呢,我们在keyCallback中增加打印来细细观察,因为WSAD操作只改变了cameraPos值,而cameraFront只有Z方向,因此cameraTarget = cameraPos + cameraFront会导致摄像机的朝向始终平行于Z轴,跟最开始的时候一样。


视角移动

欧拉角

欧拉角(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度旋转。

如果把yawpitch的初始值设置为0,你会发现一进来动一下鼠标就变成了空白,那是因为我们的相机朝向出了问题,而相机的朝向就是cameraFront决定的,那么初始值怎么设置呢,cameraFront的初始值为(0,0,-1),鼠标进到屏幕后应该是线性变化的,不能是突变,所以yawpitch的初始值就是使cameraFront = (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

滚轮控制缩放

视野(Field of View)或fov定义了我们可以看到场景中多大的范围。当视野变小时,场景投影出来的空间就会减小,产生放大(Zoom In)了的感觉。我们会使用鼠标的滚轮来放大。与鼠标移动、键盘输入一样,我们需要一个鼠标滚轮的回调函数,当滚动鼠标滚轮的时候,yoff 值代表我们竖直滚动的大小。当scrollCallback函数被调用后,我们改变全局变量fov变量的内容。因为45.0f是默认的视野值,我们将会把缩放级别(Zoom Level)限制在1.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+s就保存一次。
可以使用gl.ReadPixels()函数,我们知道glfw使用的是双缓冲区,该函数会读取前缓冲区的数据。
func ReadPixels(x int32, y int32, width int32, height int32, format uint32, xtype uint32, pixels unsafe.Pointer)

x和y表示起始点坐标,窗口的左下角为(0,0),向上Y正,向右X正;然后就是要截取的宽高,最后三个参数跟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()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/697948.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp的表单校验方式整理

uniapp的表单校验方式整理 这里我使用的模板为&#xff1a; 第一种&#xff1a; uniapp本身自带表单校验的js文件&#xff0c;代码写的很简洁&#xff0c;也是比较全面的 只要按照规则校验即可&#xff0c;下面是对应的校验代码&#xff1a; /** 数据验证&#xff08;表…

Jenkins 2.4 创建子节点

在 Dashboard > 系统管理 > 节点列表 页面&#xff0c;选择 New Node 按钮&#xff0c;新建节点 windows_10&#xff0c;节点类型选择 固定节点&#xff0c;点击 Create 创建&#xff1a; 将 远程工作目录 设置子节点电脑上的执行工作目录&#xff0c;例如在D盘创建一个 …

Django框架-3

使用admin后台管理数据 创建模型类 模型类&#xff1a;应用中的models.py文件中 from django.db import models# Create your models here. # 模型类必须要继承models.Model class doogs(models.Model):# 属性create_time models.DateTimeField(auto_now_addTrue, verbose_…

新闻丨INDEMIND荣获2023年北京市朝阳区创新型中小型企业认定

近日&#xff0c;北京市朝阳区科学技术和信息化局公布了“朝阳区2023年度第五批创新型中小企业名单”&#xff0c;INDEMIND凭借专业研发和创新能力等优势&#xff0c;成功入选。 创新型中小企业名单依据工业和信息化部2022年6月发布《优质中小企业梯度培育管理暂行办法》&#…

Golang每日一练(leetDay0111) 摆动排序II\I Wiggle Sort

目录 324. 摆动排序 II Wiggle Sort ii &#x1f31f;&#x1f31f; 280. 摆动排序 I Wiggle Sort i &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每…

威胁建模之绘制数据流图

0x00 前言 1、什么是威胁建模&#xff1a; 以结构化的方式思考、记录并讨论系统存在的安全威胁&#xff0c;并针对这些威胁制定相应的消减措施。 2、为什么要威胁建模&#xff1a; &#xff08;1&#xff09;在设计阶段开展威胁建模&#xff0c;一方面可以更全面的发现系统存…

数据结构--静态链表

数据结构–静态链表 单链表 VS 静态链表 单链表:各个结点在内存中星罗棋布、散落天涯。 静态链表:分配一整片连续的内存空间&#xff0c;各个结点集中安置。 代码定义 代码一&#xff1a; #define MaxSize 10 //静态链表的最大长度 typedef struct //静态链表结构类型的定…

无限容量分布式文件存储解决方案

常见分布式文件系统 常见分布式文件系统比较 常见的分布式文件系统有GFS、HDFS 、Ceph 、GridFS 、TFS、FastDFS等。各自适用于不同的领域。 类 Google FS 都支持文件冗余备份&#xff0c;例如 Google FS、TFS 的备份数是 3。一个文件存储到哪几个存储结点&#xff0c;通常采…

《计算机系统与网络安全》第一章 计算机系统与网络安全概述

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

java的注解方式和xml方式这两种方式对数据库进行操作详解

首先需要引入mybatisplus包 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.1</version> </dependency>第一种注解方式&#xff1a;参数是通过#{}来接收的 p…

腾讯云服务-云点播:删除腾讯云点播录制的视频文件(保存在腾讯云是需要收费的)

文档地址&#xff1a;登录 - 腾讯云https://console.cloud.tencent.com/api/explorer?Productvod&Version2018-07-17&ActionDeleteMedia 使用python后端删除&#xff1a; import json from tencentcloud.common import credential from tencentcloud.common.profile…

kafka入门,数据有序、数据乱序(十)

数据有序 数据乱序 max.in.flight.requests.per.connection指定了生产者在接收到服务器相应之前可以发送多个消息。 kafka在1.x版本之前保证单分区有序&#xff0c;条件如下 max.in.flight.requests.per.connection1 2) kafka在1.x及以后版本保证数据单区间分区有序&#xff0…

vue 函数式(编程式) render (functional:true)

文章目录 一、文档二、区别三、使用h函数的参数解释&#xff1a;参数一&#xff1a;使用导入的组件名 参数二&#xff1a;绑定css绑定事件往组件里面传递参数动态绑定值props动态绑定值datafunctional:true到底是做什么的&#xff1f;动态绑定props 非functional:true版本 参数…

JSONUtil.toJsonStr 时间变成了时间戳

问题描述 我的接口是以Date来接收日期的&#xff0c;然后我在拿到这个对象参数后&#xff0c;通过hutool当中的JSONUtil.toJsonStr将其序列化成json字符串&#xff0c;然后存储到数据库。然后存储到数据库当中发现这个字段是时间戳。 DateTimeFormat和JsonFormat 前者是控制 请…

动态规划详解Python

动态规划 动态规划&#xff08;Dynamic Programming&#xff09;是一种用于解决复杂问题的算法设计方法。它通常用于优化问题&#xff0c;其中问题可以被分解成一系列重叠子问题&#xff0c;通过存储并重复使用已经解决过的子问题的解&#xff0c;可以避免重复计算&#xff0c…

区块链的简单认识

比特币作为区块链的应用&#xff0c;让区块链广为人知&#xff0c;如果比特币作为第一代区块链&#xff0c;则以太坊则称为第二代区块链。我们知道&#xff0c;区块链的最主要目的就是去中心化&#xff0c;比特币则成为了decentralized currency&#xff0c;去中心化在技术上依…

跨注册中心服务同步实践

博主介绍&#xff1a;✌全网粉丝4W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战、定制、远程&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面…

助你丝滑过度到 Vue3 创建工程 ②③

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; VUE3~TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f449;…

同城跑腿独立版小程序 码科跑腿小程序 支持用户端 骑手端

是独立版哦&#xff0c;不是微擎的 搭建有点复杂&#xff0c;只要一步错就会导致骑手端来单没有声音提示. 多的也不介绍了&#xff0c;不知道的朋友可以百度一下码科跑腿就知道了&#xff01;

机器学习李宏毅学习笔记33

文章目录 前言一、神经网络压缩二、Network pruning----一种network compression技术1.移除不同单位的区别2.大乐透假说 总结 前言 神经网络压缩&#xff08;一&#xff09; 类神经网络剪枝&#xff08;pruning&#xff09; 一、神经网络压缩 简化模型&#xff0c;用比较少的…