OpenGL教程 学习笔记


GLSL 语法https://blog.csdn.net/xhm01291212/article/details/79270836

python目录结构 https://www.cnblogs.com/xiao-apple36/p/8884398.html

OpenGL教程

1 概念

1.1 是什么?

一般它被认为是一个API,包含了一系列可以操作图形、图像的函数。然而,OpenGL本身并不是一个API,它仅仅是一个规范。OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。

1.2 核心模式与固定渲染管线模式

早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。

而开发者迫切希望能有更多的灵活性,固定渲染管线效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

1.3 状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

1.4 视口(Viewport)

OpenGL渲染窗口的尺寸大小,即视口(Viewport),通过调用glViewport函数来设置窗口的维度(Dimension):OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。

glViewport(0, 0, 800, 600); # 前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)

1.5 渲染(Render):从3D点云到屏幕图像的过程

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。

图形渲染管线可以被划分为两个主要部分:

  • 第一部分把你的3D坐标转换为2D坐标,

  • 第二部分是把2D坐标转变为实际的有颜色的像素。

1.6 着色器(Shader):处理数据的程序

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的

  • 顶点数据 Vertex Data

顶点数据(Vertex Data):以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。

  • 顶点属性(Vertex Attribute):

而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,比如每个顶点由一个3D位置和一些颜色值组成

  • 图元(Primitive)

为了让OpenGL知道我们的坐标和颜色值构成的到底是什么**,OpenGL需要你去指定这些数据所表示的渲染类型**。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP

image-20211102142502875

2 基本内容

image-20211102142531386

2.1 顶点输入

开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y 和 z)。OpenGL仅当3D坐标在3个轴(x、y和z)上都为[-1.0,1.0]的范围内时才处理它。所有在所谓的标准化设备坐标范围内的坐标才会最终呈现在屏幕上

屏幕坐标系,(0, 0)坐标是这个图像的中心,而不是左上角

  • X轴朝右

  • Y轴朝上

  • Z轴指向您后面,通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离

    coordinate_systems_right_handed

由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。定义为一个float数组。

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

1、标准化设备坐标——>屏幕空间坐标:glViewport视口变换(Viewport Transform)完成的。
2、屏幕空间坐标——>变换为片段——>片段着色器

image-20211103164408295

2.2 顶点着色器(Vertex Shader)

作用是坐标变换,输出经过转化之后的位置坐标

我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器

输入:顶点坐标

输出:坐标gl_Position和其它属性 vec4(),四维坐标

attribute vec3 position;      // 点云空间坐标
void main()
{
	gl_Position = vec4(scale * position,1.0);// 注意最后一个分量是用在透视除法(Perspective Division)上。
} 

相关变量

  • uniform:一致变量,全局变量,对所有顶点或片断都一样

  • attribute:顶点属性,每顶点不同

  • varying:可变变量,用于顶点、片断着色器间传递自定义数据,在图元装配和光栅化过程,varying变量会被插值处理

uniform float scale;
attribute vec2 position;      
attribute vec4 color;
varying vec4 v_color;
void main()
{
    gl_Position = vec4(position, 0.0, 1.0);
    v_color = color;
}

2.3 片段着色器(Fragment Shader)

片段着色器所做的是计算像素最后的颜色输出。在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。每个颜色分量的强度设置在[0.0,1.0]之间。

片段着色器需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。

uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。

uniform vec4 color;
void main()
{
    // 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色
    gl_FragColor = color;
}

2.4 着色器程序(Program)

着色器链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。我们必须在渲染前指定OpenGL该如何解释顶点数据,对应最后一个参数。为了让OpenGL知道我们的坐标和颜色值构成的到底是什么**,OpenGL需要你去指定这些数据所表示的渲染类型**。做出的这些提示叫做图元(Primitive)

gloo.Program(vertex, fragment, count=len(self.xyz)).draw(gl.GL_QUADS)

2.5 纹理

纹理是一个2D图片,它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

为了能够把纹理映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标,用来标明该从纹理图像的哪个部分采样。之后在图形的其它片段上进行片段插值(Fragment Interpolation)

加载纹理

使用纹理之前要做的第一件事是把它们加载到我们的应用中。正确地加载图像并生成一个纹理对象,在渲染之前先把它绑定到合适的纹理单元上:

earthTexture_01 = np.array(Image.open(".//resources//image//世界地图4.jpg"))

GPU上传上传点云纹理坐标、纹理图像

program = gloo.Program(vertex, fragment, count=len(xyz))
program['texture'] = texture

代码中实现,在球类里面已经计算了纹理坐标

def sphere(r=1.0, m=100, n=100):
    """
    计算球面点云坐标和纹理坐标
    :param r: 半径
    :param m: 经线数
    :param n: 纬线数
    :return: 坐标和纹理坐标
    """
    t = np.linspace(0, np.pi, m)
    p = np.linspace(0, 2 * np.pi, n)
    positions = []
    tex_positions = []
    normal = []
    for i in range(m - 1):
        for j in range(n - 1):
            x = r * np.sin(t[i]) * np.cos(p[j])
            y = r * np.sin(t[i]) * np.sin(p[j])
            z = r * np.cos(t[i])
            positions.append([x, y, z])
            tex_positions.append([p[j] / np.pi / 2, t[i] / np.pi]) # 就相当于把整个图片按长分成2pi份,按高分成pi份

            x = r * np.sin(t[i + 1]) * np.cos(p[j])
            y = r * np.sin(t[i + 1]) * np.sin(p[j])
            z = r * np.cos(t[i + 1])
            positions.append([x, y, z])
            tex_positions.append([p[j] / np.pi / 2, t[i + 1] / np.pi])

            x = r * np.sin(t[i + 1]) * np.cos(p[j + 1])
            y = r * np.sin(t[i + 1]) * np.sin(p[j + 1])
            z = r * np.cos(t[i + 1])
            positions.append([x, y, z])
            tex_positions.append([p[j + 1] / np.pi / 2, t[i + 1] / np.pi])

            x = r * np.sin(t[i]) * np.cos(p[j + 1])
            y = r * np.sin(t[i]) * np.sin(p[j + 1])
            z = r * np.cos(t[i])
            positions.append([x, y, z])
            tex_positions.append([p[j + 1] / np.pi / 2, t[i] / np.pi])
    positions = np.array(positions)
    tex_positions = np.array(tex_positions)
    normal = np.array(positions)
    return positions, tex_positions, normal

3 坐标变换

3.1 矩阵运算

3.1.1 向量相乘

两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。但是在相乘的时候我们有两种特定情况可以选择:一个是点乘(Dot Product),记作v¯⋅k¯v¯⋅k¯,另一个是叉乘(Cross Product),记作v¯×k¯v¯×k¯

点乘

两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:

image-20211103164436838

image-20211103164615580

那么要计算两个单位向量间的夹角,我们可以使用反余弦函数cos−1 ,可得结果是143.1度。现在我们很快就计算出了这两个向量的夹角。点乘会在计算光照的时候非常有用。

image-20211103164701008

叉乘

叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,**生成一个正交于两个输入向量的第三个向量。**如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。接下来的教程中这会非常有用。下面的图片展示了3D空间中叉乘的样子:

img

两个正交向量A和B叉积,输出得到一个正交于两个输入向量的第三个向量

image-20211103164808520

3.1.2 矩阵

数乘

image-20211103164949576

现在我们也就能明白为什么这些单独的数字要叫做**标量(Scalar)了。简单来说,标量就是用它的值缩放(Scale)**矩阵的所有元素,上面中所有的元素都被放大了2倍。

单位矩阵

这种变换矩阵使一个向量完全不变:

image-20211103165341285

缩放矩阵

如果我们把缩放变量表示为(S1,S2,S3)我们可以为任意向量(x,y,z)定义一个缩放矩阵:

image-20211103172340368

位移矩阵

对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz),我们就能把位移矩阵定义为:这才是把3维坐标变成四维坐标的原因:方便对左边通过矩阵进行平移操作,有了位移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。

image-20211103172440371

旋转矩阵

在3D空间中旋转需要定义一个角一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。使用三角学,给定一个角度,可以把一个向量变换为一个经过旋转的新向量。这通常是使用一系列正弦和余弦函数(一般简称sin和cos)各种巧妙的组合得到的。.转半圈会旋转360/2 = 180度,向右旋转1/5圈表示向右旋转360/5 = 72度。

img

image-20211103172922246

矩阵的转置

img

矩阵的逆

img

3.2 坐标运算

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵

image-20211103173313795

  • 局部坐标:以对象自己为中心,是对象相对于局部原点的坐标。

  • 世界空间坐标:物体在一个更大的空间所处的坐标,可以确定与其他物体的相对位置,和其它物体一起相对于世界的原点进行摆放。

  • **观察空间坐标:以观察者为坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。**确定对于观察者来讲的相对位置关系

  • **投影坐标:**投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。

  • **视口变换:**将裁剪坐标变换为屏幕坐标,视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通

如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通。

3.2.1 局部空间

局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。

img

3.2.2 模型矩阵:局部——世界坐标

如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。这就是你希望物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。

模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。

你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。

img

glm.translate(m, self.orbit[0], self.orbit[1], self.orbit[2])  # 公转,
# translate表示瞬时坐标的位移变换经过怎样的变换能得到单位矩阵,而这个返回的变换矩阵就是就得到模型矩阵
# 所以求model矩阵的思路是,算出局部坐标的瞬时位置,反推局部——世界的位移矩阵
# glm::translate() 创建一个位移矩阵,第一个参数是目标矩阵,第二个参数是位移的方向向量
# 返回的矩阵是能实现位移的矩阵,
3.2.3 视图矩阵:世界——观察坐标

观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。

img

3.2.4 投影矩阵:观察——裁剪空间坐标

为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉

3.2.5 正射投影

效果不真实

定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标

image-20211103201135786

3.2.6 透视投影

透视(Perspective):离你越远的东西看起来更小。

perspective

OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小这是也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。

image-20211103201328296

创建了一个定义了可视空间的大平截头体,它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

 perspective_frustum

perspective 函数使用

glm::perspective(float fovy, float aspect, float zNear, float zFar);

  • 第一个参数为视锥上下面之间的夹角

  • 第二个参数为宽高比,即视窗的宽/高

  • 第三第四个参数分别为近截面和远界面的深度

glm.perspective(P_cam.alpha, 0.5 * W / float(H), 1, 300000.0)
    
def perspective(fovy, aspect, znear, zfar):
    """Create perspective projection matrix

    Parameters
    ----------
    fovy : float
        The field of view along the y axis.
    aspect : float
        Aspect ratio of the view.
    znear : float
        Near coordinate of the field of view.
    zfar : float
        Far coordinate of the field of view.

所以整体的坐标变换就是这样

image-20211103201611276

uniform float scale;          // 模型缩放因子
uniform mat4 model;           // 模型矩阵
uniform mat4 view;            // 视图矩阵
uniform mat4 projection;      // 投影矩阵
uniform mat4 viewport;        // 视口矩阵 
   
attribute vec3 position;      // 点云空间坐标

void main(){
    gl_Position = viewport * projection * view * model * vec4(scale * position,1.0);
}

3.3 摄像机

当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要创建一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

img

摄像机位置

摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动。

Z0 = 200
eyeAt = np.array([0, 0, Z0])

摄像机方向

指的是摄像机指向哪个方向。让摄像机指向场景原点:(0, 0, 0)

用场景原点向量减去摄像机位置向量的结果就是摄像机的指向向量。由于我们知道摄像机指向z轴负方向,但我们希望方向向量(Direction Vector)指向摄像机的z轴正方向。如果我们交换相减的顺序,我们就会获得一个指向摄像机正z轴方向的向量:

lookAt = np.array([0, 0, 0])
# cam - target 就是获得的摄像机方向

方向向量(Direction Vector)并不是最好的名字,因为它实际上指向从它到目标向量的相反方向(译注:注意看前面的那个图,蓝色的方向向量大概指向z轴的正方向,与摄像机实际指向的方向是正好相反的)。

右轴

它代表摄像机空间的x轴的正方向。把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));

上轴

一个指向摄像机的正y轴向量,现在我们已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:

eyeUp = np.array([0, 1, 0])
3.3.1 Look At

将所有坐标变换到摄像机的视图矩阵

现在我们有了3个相互垂直的轴和一个定义摄像机空间的位置坐标,用矩阵来表示这个坐标轴,用这个矩阵乘以任何向量来将其变换到那个坐标空间。这正是LookAt矩阵所做的

image-20211103202537755

其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量。注意,位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向。**把这个LookAt矩阵作为观察矩阵可以很高效地把所有世界坐标变换到刚刚定义的观察空间。**LookAt矩阵就像它的名字表达的那样:它会创建一个看着(Look at)给定目标的观察矩阵。

def view(cam, tar, u): #u是上
    """
    计算视图矩阵:将世界坐标系的坐标变换到观察坐标系,也就是相机的视图当中
    """
    cam = np.array(cam, np.float32)
    tar = np.array(tar, np.float32)
    u = np.array(u, np.float32)

    f = tar - cam             # 摄像机方向,从原点指向摄像机的坐标
    f = f/np.linalg.norm(f)   # 求求二范数,也就是模长
    u = u/np.linalg.norm(u)   
    s = np.cross(f, u)        # 返回两个向量的叉积。 得到摄像机坐标的右轴
    u = np.cross(s, f)        # 得到摄像机坐标的上轴
    R = np.array([[ s[0], s[1], s[2], 0],   # R代表三维坐标轴
                  [ u[0], u[1], u[2], 0],
                  [-f[0],-f[1],-f[2], 0],
                  [    0,    0,    0, 1]
                 ])  
    T = np.array([[ 1, 0, 0, -cam[0]],      # T代表相机的坐标
                  [ 0, 1, 0, -cam[1]],
                  [ 0, -0, 1, -cam[2]],
                  [ 0,  0, 0, 1]
                   ])

    v2 = np.matmul(R, T)      # 矩阵相乘既得到了lookAt矩阵也就是我们的观察矩阵
    v2 = np.transpose(v2)     # 转置,行变列

    return v2

在代码中,我们是将LookAt矩阵直接赋值给view矩阵的,然后把view传递到vertex shader中。那么view矩阵是干嘛的呢?view是负责把世界坐标系转换成用摄像机的视角所观察到的坐标系当中。

LookAt

快速构建摄像机坐标系的方法(构建结果就是这个LookAt矩阵)

坐标系空间变换方法(世界空间->观察空间)。用LookAt矩阵左乘某向量X,就可以将X从世界空间变换到观察空间。

3.3.2 欧拉角

欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,俯仰角是描述我们如何往上或往下看的角,可以在第一张图中看到。第二张图展示了偏航角,偏航角表示我们往左和往右看的程度。滚转角代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。

image-20211103203656025

对于我们的摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角。给定一个俯仰角和偏航角,我们可以把它们转换为一个代表新的方向向量的3D向量。我们可以看到x分量取决于cos(yaw)的值,z值同样取决于偏航角的正弦值。这样我们就有了一个可以把俯仰角和偏航角转化为用来自由旋转视角的摄像机的3维方向向量了

image-20211103203709045

# 设置视点位置和方向
Z0 = 200
eyeAt = np.array([0, 0, Z0]) # cam
lookAt = np.array([0, 0, 0]) # 目标卫星
eyeUp = np.array([0, 1, 0])  # 自转轴
view_0 = myMath.view(eyeAt, lookAt, eyeUp)#计算视图矩阵

def view(cam, tar, u):
    """
    计算视图矩阵
    """
    cam = np.array(cam, np.float32)
    tar = np.array(tar, np.float32)
    u = np.array(u, np.float32)

    f = tar - cam
    f = f/np.linalg.norm(f)
    u = u/np.linalg.norm(u)
    s = np.cross(f, u)
    u = np.cross(s, f)
    R = np.array([[ s[0], s[1], s[2], 0],
                  [ u[0], u[1], u[2], 0],
                  [-f[0],-f[1],-f[2], 0],
                  [    0,    0,    0, 1]
                 ])
    T = np.array([[ 1, 0, 0, -cam[0]],
                  [ 0, 1, 0, -cam[1]],
                  [ 0, -0, 1, -cam[2]],
                  [ 0,  0, 0, 1]
                   ])

    v2 = np.matmul(R, T)
    v2 = np.transpose(v2)
    # print(v2)
    return v2

4 光照

当我们在OpenGL中创建一个光源时,我们希望给光源一个颜色。我们将光源设置为白色。当我们把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色),使用不同的光源颜色来显现不同的颜色。

uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
    FragColor = vec4(lightColor * objectColor, 1.0);
}
def __init__(self, color=(1.0, 1.0, 0, 1.0)): # 构造器传入
    self.fragment = """
        void main(){
            gl_FragColor = vec4"""+str(color)+""";
        } """

4.1 光照模型

OpenGL的光照使用的是简化的光照模型,对现实的情况进行近似,这样处理起来会更容易一些,而且看起来也差不多一样。这些光照模型都是基于我们对光的物理特性的理解。

比如冯氏光照模型(Phong Lighting Model)。冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子:

img

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
4.1.1 环境光照

我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中,实现即便场景中没有直接的光源也能看起来存在一些发散的光。

void main()
{
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;

    vec3 result = ambient * objectColor;
    FragColor = vec4(result, 1.0);
}
4.1.2 漫反射光照

漫反射是指光线被粗糙表面无规则地向各个方向反射的现象。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,就弥漫地射向不同方向。

img

图左上方有一个光源,它所发出的光线落在物体的一个片段上。为了测量光线和片段的角度,我们使用一个叫做**法向量(Normal Vector)**的东西,它是垂直于片段表面的一个向量(这里以黄色箭头表示),这两个向量之间的角度很容易就能够通过点乘计算出来,我们知道两个单位向量的夹角越小,它们点乘的结果越倾向于1。θ越大,光对片段颜色的影响就应该越小。

img

所以,计算漫反射光照需要什么?

  • 法向量:一个垂直于顶点表面的向量。
  • 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。为了计算这个光线,我们需要光的位置向量和片段的位置向量。
// 环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色,来获得片段最后的输出颜色。              
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);

注意

目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们是不是应该把法向量也转换为世界空间坐标?基本正确,但是这不是简单地把它乘以一个模型矩阵就能搞定的。

首先,法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。因此,如果我们打算把法向量乘以一个模型矩阵,我们就要从矩阵中移除位移部分,只选用模型矩阵左上角3×3的矩阵。对于法向量,我们只希望对它实施缩放和旋转变换。

其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:

img

每当我们应用一个不等比缩放时,法向量就不会再垂直于对应的表面了,这样光照就会被破坏。

修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵,大部分的资源都会将法线矩阵定义为应用到模型-观察矩阵上的操作,但是由于我们只在世界空间中进行操作(不是在观察空间),我们只使用模型矩阵。

Normal = mat3(transpose(inverse(model))) * aNormal;

在漫反射光照部分,光照表现并没有问题,这是因为我们没有对物体本身执行任何缩放操作,所以并不是必须要使用一个法线矩阵,仅仅让模型矩阵乘以法线也可以。可是,如果你进行了不等比缩放,使用法线矩阵去乘以法向量就是必不可少的了。

4.1.3 镜面光照

镜面光照也是依据光的方向向量和物体的法向量来决定的,当我们去看光被物体所反射的那个方向的时候,我们会看到一个高光。观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。之后,我们计算镜面光强度,用它乘以光源的颜色,再将它加上环境光和漫反射分量。

img

下一步,我们计算视线方向向量,和对应的沿着法线轴的反射向量:

vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);

需要注意的是我们对lightDir向量进行了取反。reflect函数要求第一个向量是光源指向片段位置的向量,但是lightDir当前正好相反,是从片段指向光源(由先前我们计算lightDir向量时,减法的顺序决定)。为了保证我们得到正确的reflect向量,我们通过对lightDir向量取反来获得相反的方向。第二个参数要求是一个法向量,所以我们提供的是已标准化的norm向量。

一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:

img

positions = np.array(positions)   //非常奇怪?法向直接等于点云坐标?
normal = np.array(positions)
sun_direction = np.array(sat[0].orbit_xyz(Recorder.t)) - np.array(sat[1].orbit_xyz(Recorder.t)) //光的位置减片段在地心赤道春分坐标系中的卫星和相机瞬时坐标

vertex = """ 
            attribute vec3 normal;        // 点云法向量    (1*3)
            varying vec3 v_normal;        // 向片断着色程序传递法向量

            void main(){
                vec4 n = model * vec4(normal,0.0);  // model是模型矩阵,定义了自我中心坐标轴转化成世界坐标的一系列变换
                v_normal = normalize(vec3(n[0],n[1],n[2]));//归一化处理保证点乘结果就是夹角
            } """
                
fragment = """
            varying vec3 v_normal; 
            
            void main(){
                float cosine = max(0.0,dot(normalize(sun_direction),v_normal)); // 太阳光线的方向点乘法向就是夹角,点乘结果就是镜面发射影响。结果值再乘以光的颜色,。两个向量之间的角度越大,反射分量就会越小:
                float i_r = 3*(c[0]*cosine+0.00*c[0])/3.14;  // c 代表的是color的太阳光数组
                float i_g = 3*(c[1]*cosine+0.00*c[1])/3.14;  
                float i_b = 3*(c[2]*cosine+0.00*c[2])/3.14; 
                color = vec4(i_r,i_g,i_b,1.0);

                gl_FragColor = color;

            } """

4.2 材质

4.3 光源

4.3.1 平行光

当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。因为所有的光线都是平行的,所以物体与光源的相对位置是不重要的,因为对场景中每一个物体光的方向都是一致的。由于光的位置向量保持一致,场景中每个物体的光照计算将会是类似的。

img

我们可以定义一个光线方向向量而不是位置向量来模拟一个定向光。着色器的计算基本保持不变,但这次我们将直接使用光的direction向量而不是通过direction来计算lightDir向量。

sun_direction = np.array(Sun.orbit)

当我们将位置向量定义为一个vec4时,很重要的一点是要将w分量设置为1.0,这样变换和投影才能正确应用。然而,当我们定义一个方向向量为vec4的时候,我们不想让位移有任何的效果(因为它仅仅代表的是方向),所以我们将w分量设置为0.0。

方向向量就会像这样来表示:vec4(0.2f, 1.0f, 0.3f, 0.0f)。这也可以作为一个快速检测光照类型的工具:你可以检测w分量是否等于1.0,来检测它是否是光的位置向量;w分量等于0.0,则它是光的方向向量,这样就能根据这个来调整光照计算了:

if(lightVector.w == 0.0) // 注意浮点数据类型的误差
  // 执行定向光照计算
else if(lightVector.w == 1.0)
  // 根据光源的位置做光照计算(与上一节一样)
4.3.2 点光源

点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。

img

4.3.3 聚光源

5 模型

我们不太能够对像是房子、汽车或者人形角色这样的复杂形状手工定义所有的顶点、法线和纹理坐标。我们想要的是将这些模型(Model)导入(Import)到程序当中,模型通常都由3D艺术家在Blender、3DS Max或者Maya这样的3D建模工具制作。这些工具将会在导出到模型文件的时候自动生成所有的顶点坐标、顶点法线以及纹理坐标。

5.1 网格

通常每个模型都由几个子模型/形状组合而成。组合模型的每个单独的形状就叫做一个网格(Mesh)。比如说有一个人形的角色:艺术家通常会将头部、四肢、衣服、武器建模为分开的组件,并将这些网格组合而成的结果表现为最终的模型。一个网格是我们在OpenGL中绘制物体所需的最小单位(顶点数据、索引和材质属性)。一个模型(通常)会包括多个网格。

一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。

6 深度:遮挡

判断哪些是被遮挡的部分不应该被显示

当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。

gl_FragCoord的x和y分量代表了片段的屏幕空间坐标(其中(0, 0)位于左下角)。gl_FragCoord中也包含了一个z分量,它包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值。

7 混合:透明度

OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。一个有色玻璃窗是一个透明的物体,玻璃有它自己的颜色,但它最终的颜色还包含了玻璃之后所有物体的颜色。这也是混合这一名字的出处,我们混合(Blend)(不同物体的)多种颜色为一种颜色。所以透明度能让我们看穿物体。

img

**一个物体的透明度是通过它颜色的aplha值来决定的。**Alpha颜色值是颜色向量的第四个分量,设置为1.0,让这个物体的透明度为0.0,而当alpha值为0.0时物体将会是完全透明的。当alpha值为0.5时,物体的颜色有50%是来自物体自身的颜色,50%来自背后物体的颜色。

实现:丢弃(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中。

8 面剔除:丢弃背向面

OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面,节省我们很多的片段着色器调用。但我们仍要告诉OpenGL哪些面是正向面(Front Face),哪些面是背向面(Back Face)。OpenGL使用了一个很聪明的技巧,分析顶点数据的环绕顺序(Winding Order)。

8.1 环绕顺序

当我们定义一组三角形顶点时,我们会以特定的环绕顺序来定义它们,可能是顺时针(Clockwise)的,也可能是逆时针(Counter-clockwise)的。每个三角形由3个顶点所组成,我们会从三角形中间来看,为这3个顶点设定一个环绕顺序。

img

观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形。通过这个顺序能甄别面向还是背向

img

glCullFace函数有三个可用的选项:

  • GL_BACK:只剔除背向面。
  • GL_FRONT:只剔除正向面。
  • GL_FRONT_AND_BACK:剔除正向面和背向面。
  • 3
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值