• 【转】【OpenTK】C# OpenTK教程 着色器 layout


    图形管道

    OpenGL中所有物体处在3D空间中,但屏幕和窗口是一个2D像素数组,因此OpenGL工作的很大一部分是将所有3D坐标转换为适合您屏幕上的2D像素。将3D坐标转换为2D像素的过程由OpenGL的图形管道管理。图形管道可分为两大部分:第一部分将3D坐标转换为2D坐标,第二部分将2D坐标转换为实际彩色像素。在本教程中,我们将简要讨论图形管道,以及如何利用它来创建花哨的像素。

    图形管道将一组3D坐标作为输入,并将这些坐标转换为屏幕上的彩色2D像素。图形管道可分为几个步骤,其中每个步骤都需要上一步的输出作为输入。所有这些步骤都是高度专业化的(它们具有一个特定的功能),并且可以很容易地并行执行。由于其具有并行性特点,当今的图形卡具有数千个小型处理内核,通过为管道的每个步骤在GPU上运行小型程序,在图形管道中快速处理数据。这些小程序称为着色器。

    其中一些着色器由开发人员配置,这允许我们编写自己的着色器来替换现有的默认着色器。这为我们提供了对管道特定部分的更细粒度的控制,并且由于它们在GPU上运行,因此还可以为我们节省宝贵的CPU时间。着色器以OpenGL着色语言(GLSL)编写,我们将在下一教程中深入探讨这一点。

    下面您将找到图形管道所有阶段的抽象表示形式。

    pipeline
    具有蓝色背景的部分是可编程的,并且具有灰色背景的部分可以使用函数轻轻自定义。步骤如下:

    1. 顶点着色器:顶点移动到位置。这是应用模型位置等位置的位置。
    2. 形状拼接。在这个阶段,OpenGL的工作原理是将顶点拼接到三角形中;
    3. 几何着色器:过程的可选阶段。允许您从形状装配体微调结果。
    4. 栅格化:三角形转换为碎片。
    5. 线段着色器:对线段进行修改,以包括颜色数据等内容。这是纹理和照明,除其他外,应用的地方。
    6. 测试和混合:片段着色器的结果与场景的其余部分集成。

    这些可能看起来很繁琐,但一旦设置完成,我们进入管道,它是相当直观的。

    一些新的函数

    我们需要重写几个额外的函数才能开始。首先,我们重写OnLoad函数。

    protected override void OnLoad(EventArgs e)
    {
        GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        base.OnLoad(e);
    }

    当窗口首次打开时,此函数将运行一次。任何初始化相关的代码都应转到此处。

    同时在这里,我们得到我们OpenGL调用的第一个函数:GL.ClearColor。这需要四个浮点,范围在0.0f1.0f之间。这将决定在窗口在帧之间清除后的颜色。

    之后,我们需要重写OnRenderFrame

    protected override void OnRenderFrame(FrameEventArgs e)
    {
        GL.Clear(ClearBufferMask.ColorBufferBit);
    
        Context.SwapBuffers();
        base.OnRenderFrame(e);
    }

    我们这里有两个调用。首先,GL.Clear使用OnLoad中设置的颜色清除屏幕。这应始终是呈现时调用的第一个函数。
    之后我们使用Context.SwapBuffers。几乎任何现代OpenGL上下文都是所谓的"双缓冲"。双缓冲意味着OpenGL绘制到的两个领域。本质上:显示一个区域,而另一个区域用来展示。然后,当您调用交换缓冲区时,两者将反转。单缓冲上下文可能会有屏幕卡顿等问题。

    现在我们重写OnResize

    protected override void OnResize(EventArgs e)
    {
        GL.Viewport(0, 0, Width, Height);
        base.OnResize(e);
    }

    每次调整窗口大小时,都会运行此功能。GL.ViewportNDC映射到窗口。OnResize不是非常重要,除了我们已经写入的,这里后期将不会添加任何代码。

    顶点输入

    要开始绘制某些数据,我们必须首先给OpenGL一些输入顶点数据。OpenGL是一个3D图形库,因此我们在OpenGL中指定的所有坐标都位于3Dxyz坐标中)。OpenGL不会简单地将所有3D坐标转换为屏幕上的2D像素;当 3 个轴(xyz)上的3D坐标在 -1.0 和 1.0 之间的特定范围内时,OpenGL才处理它们。此所谓的规范化设备坐标范围内的所有坐标最终都将在屏幕上可见(并且该区域外的所有坐标不会)。

    因为我们想要渲染一个三角形,所以我们要指定三个顶点,每个顶点都有一个3D位置。我们在浮动数组中的规范化设备坐标(OpenGL的可见区域)中定义它们。在类中作为属性来表示:

    private float[] vertices =
    {
        -0.5f, -0.5f, 0.0f,     // Bottom-left vertex 
        0.5f, -0.5f, 0.0f,      // Bottom-right vertex 
        0.0f, 0.5f, 0.0f        // Top vertex
    };

    由于OpenGL3D空间中工作,因此我们渲染一个2D三角形,每个顶点具有0.0z坐标。这样,三角形的深度保持不变,使其看起来像是2D

    规范化设备坐标(NDC)

    在顶点着色器中处理顶点坐标后,它们应位于规范化设备坐标中,这是 x、y 和 z 值从 -1.0 到 1.0 变化的一个小空间。超出此范围的任何坐标都将被丢弃/剪切,并且在屏幕上不可见。下面你可以看到我们在规范化设备坐标中指定的三角形(忽略 z 轴):

    ndc

    与通常的屏幕不同,屏幕坐标是向上方向的正y轴点,而(0,0)坐标位于图形的中心,而不是左上角。最终,您希望所有(已转换的)坐标最终到达此坐标空间中,否则它们将不可见。

    然后,使用GL.Viewport提供的数据,通过视口变换将NDC坐标转换为屏幕空间坐标。然后,生成的屏幕空间坐标将转换为片段,作为片段着色器的输入。

    缓冲区

    定义顶点数据后,我们希望将其作为输入发送到图形管道的第一个过程:顶点着色器。这是通过创建GPU上的内存来完成的,我们在其中存储顶点数据,配置OpenGL应如何解释内存,并指定如何将数据发送到图形卡。然后,顶点着色器或通过我们告诉它的信息,从而在它的内存中处理尽可能多的顶点。

    我们通过所谓的顶点缓冲对象(VBO)管理此内存,该对象可以在GPU的内存中存储大量顶点。使用这些缓冲对象的优点是,我们可以一次向图形卡发送大量数据,而无需一次发送顶点数据。从CPU将数据发送到显卡相对缓慢,因此,只要我们可能,我们尝试一次发送尽可能多的数据。一旦数据进入显卡的内存,顶点着色器几乎可以即时访问顶点,使其非常快。

    顶点缓冲区对象是我们第一次出现OpenGL对象,正如我们在OpenGL教程中讨论过的。就像OpenGL中的任何对象一样,此缓冲区具有与该缓冲区对应的唯一ID,因此我们可以使用GL.GenBuffers函数生成具有缓冲区IDID

    Game类添加int用来存储句柄:

    int VertexBufferObject;

    之后在OnLoad函数内添加这一行:

    VertexBufferObject = GL.GenBuffer();

    OpenGL具有多种类型的缓冲区对象,具有顶点缓冲区对象的缓冲区类型为BufferTarget.ArrayBufferOpenGL允许我们同时绑定到多个缓冲区,只要它们具有不同的缓冲区类型。我们可以使用GL.BindBuffer函数将新创建的缓冲区绑定到BufferTarget.ArrayBuffer

    GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject); 

    从该点开始,我们进行的任何缓冲区调用(在BufferTarget.ArrayBuffer上)将用于配置当前绑定的缓冲区,即VertexBufferObject。然后我们可以调用GL.BufferData。将以前定义的顶点数据复制到缓冲区内存中的缓冲区数据函数:

    GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw); 

    GL.BufferData是一个专门用于将用户定义的数据复制到当前绑定的缓冲区的函数。它的第一个参数是我们要将数据复制到的缓冲区的类型:当前绑定到BufferTarget.ArrayBuffer区目标的顶点缓冲区对象。第二个参数指定要传递给缓冲区的数据的大小(以字节为单位);数据类型的简单大小,乘以顶点的长度,就足够了。第三个参数是我们想要发送的实际数据。

    第四个参数是BufferUsageHint,它指定我们希望图形卡如何管理给定的数据。这有 3 种形式:

    StaticDraw: 数据很可能不改变或者改变的很少. 
    DynamicDraw: 数据可能会改变很多. 
    StreamDraw: 每次绘制数据时都会更改

    三角形的位置数据不会更改,并且对于每个渲染调用都保持不变,因此其使用类型最好为StaticDraw。如果有一个缓冲区,其数据可能会频繁更改,则DynamicDrawStreamDraw的使用类型可确保图形卡将数据放在内存中,从而允许更快的写入速度。

    注意:当编程结束时,我们需要手动清除缓冲区。为此,我们需要添加以下函数:

    protected override void OnUnload(EventArgs e)
    {
        GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
        GL.DeleteBuffer(VertexBufferObject);
        base.OnUnload(e);
    }

    将缓冲区绑定到 0 基本上会将其设置为 null,因此,任何修改缓冲区而不首先绑定缓冲区的调用都会导致崩溃。这比意外修改不希望修改的缓冲区更容易调试。

    到目前为止,我们存储了图形卡内存中的顶点数据,由名为 VBO 的顶点缓冲区对象管理。接下来,我们要创建一个顶点和片段着色器,实际处理这些数据,所以让我们开始构建这些

    着色

    现在,我们已经拥有了数据,是时候创建我们的管道了。为此,我们创建顶点着色器和线段着色器。

    顶点着色器是像我们这样的人可编程的着色器之一。现代OpenGL要求我们至少设置一个顶点和片段着色器,如果我们想要做一些渲染,我们将简要介绍着色器,并配置两个非常简单的着色器来绘制我们的第一个三角形。在下一教程中,我们将更详细地讨论着色器。

    我们需要做的第一件事是在着色器语言GLSLOpenGL着色语言)中编写顶点着色器,然后编译此着色器,以便我们可以在应用程序中使用它。下面您将在GLSL中找到非常基本的顶点着色器的源代码:

    #version 330 core
    layout (location = 0) in vec3 aPosition
    
    void main()
    {
        gl_Position = vec4(aPosition, 1.0);
    }

    将之另存为shader.vert

    如您所看到的,GLSL看起来与C类似。每个着色器以其版本的声明开头。由于OpenGL 3.3及更高版本号与OpenGL的版本号匹配(例如,GLSL版本420对应于OpenGL版本4.2)。我们还明确提到我们使用的核心配置文件功能。

    接下来,我们使用in关键字声明顶点着色器中的所有输入顶点属性。现在,我们只关心位置数据,因此我们只需要一个顶点属性。GLSL具有一个矢量数据类型,该数据类型基于其后缀数字包含1到4个浮点。由于每个顶点都有一个3D坐标,因此我们创建一个带aPositionvec3输入变量。我们还通过布局(location = 0)专门设置输入变量的位置,稍后您将看到为什么我们需要该位置。

    每个着色器的入口点都是void main()函数。在这里您可以根据自己所需做任何处理。但是,在这里,我们只需将一个用于顶点着色器的内置的、表示该顶点的最终位置的变量gl_Position进行赋值。但是,gl_Position是一个 vec4,但我们的输入顶点是一个 vec3 。为此,我们使用函数vec4使向量足够长。

    当前顶点着色器可能是我们可以想象到的最简单的顶点着色器,因为我们没有处理任何输入数据,只是将它转发到着色器的输出。在实际应用中,输入数据通常尚未在规范化的设备坐标中,因此我们首先必须转换输入数据以使位于OpenGL可见区域内的坐标。

    片段着色器是我们要为渲染三角形而创建的第二个也是最后一个着色器。片段着色器用于计算像素的颜色输出。为了简单,片段着色器将始终输出橙色。

    计算机图形中的颜色表示为4个值的矢量:红色、绿色、蓝色和alpha(不透明度)分量,通常缩写为 RGBA。在 OpenGLGLSL中定义颜色时,我们将每个组件的强度设置为介于0.0和 1.0 之间的值。例如,如果我们将红色设置为 1.0f,将绿色设置为 1.0f,我们就会得到两种颜色的混合物,并得到黄色。通过这3种颜色组件,我们可以产生超过1600万种不同的颜色!

    #version 330 core
    out vec4 FragColor;
    void main()
    {
        FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
    }

    将之另存为shader.frag

    片段着色器只需要一个输出变量,它是大小 4 的矢量,用于定义我们应该自己计算的最终颜色输出。我们可以用 out 关键字声明输出值,我们在这里立即命名为 FragColor。接下来,我们只需将vec4分配给颜色输出,作为橙色,Alpha值为 1.0(1.0 完全不透明)。

    编译着色器

    我们有着色器源,但现在我们需要编译着色器。这在运行时完成;无法预先编译着色器并打包程序,因为编译的着色器取决于许多因素,如图形卡模型、制造商和驱动程序。相反,我们包括着色器源代码,并在程序开始时编译它。

    我们将通过创建一个着色器类来做到这一点,该类编译着色器并包装几个函数,我们将在稍后看到。

    public class Shader
    {
        int Handle;
    
        public Shader(string vertexPath, string fragmentPath)
        {
    
        }
    }

    句柄将表示我们最终着色器程序在编译完成后的位置。我们将在构造函数中进行所有初始化。

    首先,在构造函数中,定义两个 int:VertexShaderFragmentShader。这些是各个着色器的句柄。它们在构造函数中定义,因为在完整着色器程序完成后,我们不需要单独的着色器。

    接下来,我们需要从各个着色器文件加载源代码。我们可以像这样做:

    string VertexShaderSource;
    
    using (StreamReader reader = new StreamReader(vertexPath, Encoding.UTF8))
    {
        VertexShaderSource = reader.ReadToEnd();
    }
    
    string FragmentShaderSource;
    
    using (StreamReader reader = new StreamReader(fragmentPath, Encoding.UTF8))
    {
        FragmentShaderSource = reader.ReadToEnd();
    }

    然后,我们生成着色器,并将源代码绑定到着色器。

    VertexShader = GL.CreateShader(ShaderType.VertexShader);
    GL.ShaderSource(VertexShader, VertexShaderSource);
    
    FragmentShader = GL.CreateShader(ShaderType.FragmentShader);
    GL.ShaderSource(FragmentShader, FragmentShaderSource);

    然后,我们编译着色器并检查错误。

    GL.CompileShader(VertexShader);
    
    string infoLogVert = GL.GetShaderInfoLog(VertexShader);
    if (infoLogVert != System.String.Empty)
        System.Console.WriteLine(infoLogVert);
    
    GL.CompileShader(FragmentShader);
    
    string infoLogFrag = GL.GetShaderInfoLog(FragmentShader);
    
    if (infoLogFrag != System.String.Empty)
        System.Console.WriteLine(infoLogFrag);

    如果在编译时出现任何错误,可以使用函数GL.GetShaderInfoLog获取调试字符串。假设没有问题,我们可以继续链接。

    GL.DetachShader(Handle, VertexShader);
    GL.DetachShader(Handle, FragmentShader);
    GL.DeleteShader(FragmentShader);
    GL.DeleteShader(VertexShader);

    我们现在有一个有效的着色器,所以让我们添加一种方法来使用它。将此函数添加Shader类:

    void Use()
    {
        GL.UseProgram(Handle);
    }

    最后,我们需要在此类使用完成后清理句柄。由于面向对象语言问题,无法在最终化器中完成。相反,我们必须从 IDisposable 派生,并记住手动调用着色器上的Dispose。在代码的其余部分下方添加以下内容:

    private bool disposedValue = false;
    
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            GL.DeleteProgram(Handle);
    
            disposedValue = true;
        }
    }
    
    ~Shader()
    {
        GL.DeleteProgram(Handle);
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    祝贺!我们现在有一个功能齐全的着色器类

    回到Game类中,添加新属性Shader shader;然后,在OnLoad中,添加shader = new Shader("shader.vert", "shader.frag");。然后,转到 OnUnload,然后添加行shader.Dispose();

    尝试运行;如果没有打印到控制台, 您的着色器已正确编译!

    链接顶点属性

    顶点数组对象

    增编:动态检索着色器布局

    原文地址:https://blog.csdn.net/u014786187/article/details/109356789

  • 相关阅读:
    行动- 一桌菜,十几盘,有荤有素,有凉有热,怎么吃呢?你可以找一盘看起来好吃的,也可以找一盘离自己近的,都行。但是,得动筷子。不管怎么吃,最重要的是得动筷子。学技术也是一样。 有的人死活不动筷子,还不断念叨:“我要开始吃了。我马上就要开始吃了。我只要开始吃就能吃饱。我吃饱了就不饿了。你能不能告诉我该先吃哪一盘?先冷盘后热菜再喝汤这样是不是最好?但是我听说广东人都是先...
    行动
    互相牵制的能力
    我的信仰
    my life
    PDCA循环
    命运
    健身 赚钱 ; 旅行 用心爱一个人就行了 其他的都会开挂来临~
    灵魂
    陪伴的意义
  • 原文地址:https://www.cnblogs.com/mqxs/p/14133098.html
Copyright © 2020-2023  润新知