C# OpenTK教程 - 1.2 你好三角形
图形管道
在OpenGL
中所有物体处在3D
空间中,但屏幕和窗口是一个2D
像素数组,因此OpenGL
工作的很大一部分是将所有3D
坐标转换为适合您屏幕上的2D
像素。将3D
坐标转换为2D
像素的过程由OpenGL
的图形管道管理。图形管道可分为两大部分:第一部分将3D
坐标转换为2D
坐标,第二部分将2D
坐标转换为实际彩色像素。在本教程中,我们将简要讨论图形管道,以及如何利用它来创建花哨的像素。
图形管道将一组3D
坐标作为输入,并将这些坐标转换为屏幕上的彩色2D
像素。图形管道可分为几个步骤,其中每个步骤都需要上一步的输出作为输入。所有这些步骤都是高度专业化的(它们具有一个特定的功能),并且可以很容易地并行执行。由于其具有并行性特点,当今的图形卡具有数千个小型处理内核,通过为管道的每个步骤在GPU
上运行小型程序,在图形管道中快速处理数据。这些小程序称为着色器。
其中一些着色器由开发人员配置,这允许我们编写自己的着色器来替换现有的默认着色器。这为我们提供了对管道特定部分的更细粒度的控制,并且由于它们在GPU
上运行,因此还可以为我们节省宝贵的CPU
时间。着色器以OpenGL
着色语言(GLSL
)编写,我们将在下一教程中深入探讨这一点。
下面您将找到图形管道所有阶段的抽象表示形式。
具有蓝色背景的部分是可编程的,并且具有灰色背景的部分可以使用函数轻轻自定义。步骤如下:
- 顶点着色器:顶点移动到位置。这是应用模型位置等位置的位置。
- 形状拼接。在这个阶段,
OpenGL
的工作原理是将顶点拼接到三角形中; - 几何着色器:过程的可选阶段。允许您从形状装配体微调结果。
- 栅格化:三角形转换为碎片。
- 线段着色器:对线段进行修改,以包括颜色数据等内容。这是纹理和照明,除其他外,应用的地方。
- 测试和混合:片段着色器的结果与场景的其余部分集成。
这些可能看起来很繁琐,但一旦设置完成,我们进入管道,它是相当直观的。
一些新的函数
我们需要重写几个额外的函数才能开始。首先,我们重写OnLoad
函数。
protected override void OnLoad(EventArgs e)
{GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);base.OnLoad(e);
}
当窗口首次打开时,此函数将运行一次。任何初始化相关的代码都应转到此处。
同时在这里,我们得到我们OpenGL
调用的第一个函数:GL.ClearColor
。这需要四个浮点,范围在0.0f
和1.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.Viewport
将NDC
映射到窗口。OnResize
不是非常重要,除了我们已经写入的,这里后期将不会添加任何代码。
顶点输入
要开始绘制某些数据,我们必须首先给OpenGL
一些输入顶点数据。OpenGL
是一个3D
图形库,因此我们在OpenGL
中指定的所有坐标都位于3D
(x
、y
和z
坐标中)。OpenGL
不会简单地将所有3D
坐标转换为屏幕上的2D
像素;当 3 个轴(x
、y
和z
)上的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
};
由于OpenGL
在3D
空间中工作,因此我们渲染一个2D
三角形,每个顶点具有0.0
的z
坐标。这样,三角形的深度保持不变,使其看起来像是2D
。\
规范化设备坐标(NDC)
在顶点着色器中处理顶点坐标后,它们应位于规范化设备坐标中,这是 x、y 和 z 值从 -1.0 到 1.0 变化的一个小空间。超出此范围的任何坐标都将被丢弃/剪切,并且在屏幕上不可见。下面你可以看到我们在规范化设备坐标中指定的三角形(忽略 z 轴):
与通常的屏幕不同,屏幕坐标是向上方向的正y
轴点,而(0,0)
坐标位于图形的中心,而不是左上角。最终,您希望所有(已转换的)坐标最终到达此坐标空间中,否则它们将不可见。
然后,使用GL.Viewport
提供的数据,通过视口变换将NDC
坐标转换为屏幕空间坐标。然后,生成的屏幕空间坐标将转换为片段,作为片段着色器的输入。
缓冲区
定义顶点数据后,我们希望将其作为输入发送到图形管道的第一个过程:顶点着色器。这是通过创建GPU
上的内存来完成的,我们在其中存储顶点数据,配置OpenGL
应如何解释内存,并指定如何将数据发送到图形卡。然后,顶点着色器或通过我们告诉它的信息,从而在它的内存中处理尽可能多的顶点。
我们通过所谓的顶点缓冲对象(VBO
)管理此内存,该对象可以在GPU
的内存中存储大量顶点。使用这些缓冲对象的优点是,我们可以一次向图形卡发送大量数据,而无需一次发送顶点数据。从CPU
将数据发送到显卡相对缓慢,因此,只要我们可能,我们尝试一次发送尽可能多的数据。一旦数据进入显卡的内存,顶点着色器几乎可以即时访问顶点,使其非常快。
顶点缓冲区对象是我们第一次出现OpenGL
对象,正如我们在OpenGL
教程中讨论过的。就像OpenGL
中的任何对象一样,此缓冲区具有与该缓冲区对应的唯一ID
,因此我们可以使用GL.GenBuffers
函数生成具有缓冲区ID
的ID
。
向Game
类添加int
用来存储句柄:
int VertexBufferObject;
之后在OnLoad
函数内添加这一行:
VertexBufferObject = GL.GenBuffer();
OpenGL
具有多种类型的缓冲区对象,具有顶点缓冲区对象的缓冲区类型为BufferTarget.ArrayBuffer
。OpenGL
允许我们同时绑定到多个缓冲区,只要它们具有不同的缓冲区类型。我们可以使用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
。如果有一个缓冲区,其数据可能会频繁更改,则DynamicDraw
或StreamDraw
的使用类型可确保图形卡将数据放在内存中,从而允许更快的写入速度。
注意:当编程结束时,我们需要手动清除缓冲区。为此,我们需要添加以下函数:
protected override void OnUnload(EventArgs e)
{GL.BindBuffer(BufferTarget.ArrayBuffer, 0);GL.DeleteBuffer(VertexBufferObject);base.OnUnload(e);
}
将缓冲区绑定到 0 基本上会将其设置为 null,因此,任何修改缓冲区而不首先绑定缓冲区的调用都会导致崩溃。这比意外修改不希望修改的缓冲区更容易调试。
到目前为止,我们存储了图形卡内存中的顶点数据,由名为 VBO
的顶点缓冲区对象管理。接下来,我们要创建一个顶点和片段着色器,实际处理这些数据,所以让我们开始构建这些
着色
现在,我们已经拥有了数据,是时候创建我们的管道了。为此,我们创建顶点着色器和线段着色器。
顶点着色器是像我们这样的人可编程的着色器之一。现代OpenGL
要求我们至少设置一个顶点和片段着色器,如果我们想要做一些渲染,我们将简要介绍着色器,并配置两个非常简单的着色器来绘制我们的第一个三角形。在下一教程中,我们将更详细地讨论着色器。
我们需要做的第一件事是在着色器语言GLSL
(OpenGL
着色语言)中编写顶点着色器,然后编译此着色器,以便我们可以在应用程序中使用它。下面您将在GLSL
中找到非常基本的顶点着色器的源代码:
#version 330 core
layout (location = 0) in vec3 aPositionvoid 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
坐标,因此我们创建一个带aPosition
的vec3
输入变量。我们还通过布局(location = 0
)专门设置输入变量的位置,稍后您将看到为什么我们需要该位置。
每个着色器的入口点都是void main()
函数。在这里您可以根据自己所需做任何处理。但是,在这里,我们只需将一个用于顶点着色器的内置的、表示该顶点的最终位置的变量gl_Position
进行赋值。但是,gl_Position
是一个 vec4
,但我们的输入顶点是一个 vec3
。为此,我们使用函数vec4
使向量足够长。
当前顶点着色器可能是我们可以想象到的最简单的顶点着色器,因为我们没有处理任何输入数据,只是将它转发到着色器的输出。在实际应用中,输入数据通常尚未在规范化的设备坐标中,因此我们首先必须转换输入数据以使位于OpenGL
可见区域内的坐标。
片段着色器是我们要为渲染三角形而创建的第二个也是最后一个着色器。片段着色器用于计算像素的颜色输出。为了简单,片段着色器将始终输出橙色。
计算机图形中的颜色表示为4个值的矢量:红色、绿色、蓝色和alpha
(不透明度)分量,通常缩写为 RGBA
。在 OpenGL
或GLSL
中定义颜色时,我们将每个组件的强度设置为介于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:VertexShader
和FragmentShader
。这些是各个着色器的句柄。它们在构造函数中定义,因为在完整着色器程序完成后,我们不需要单独的着色器。
接下来,我们需要从各个着色器文件加载源代码。我们可以像这样做:
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();
。
尝试运行;如果没有打印到控制台, 您的着色器已正确编译!
链接顶点属性
顶点着色器允许我们以顶点属性的形式指定所需的任何输入。 尽管这提供了极大的灵活性,但这确实意味着我们必须手动指定输入数据的哪一部分去顶点着色器中的哪个顶点属性。 这意味着我们必须指定OpenGL
在渲染之前应如何解析顶点数据。
我们的顶点缓冲区数据的格式如下:
- 位置数据存储为32位(4字节)浮点值。
- 每个位置均由这些值中的3个组成。
- 每组3个值之间没有空格(或其他值)。 这些值紧密封装在数组中。
- 数据中的第一个值在缓冲区的开头。
有了这些知识,我们可以告诉OpenGL
如何使用GL.VertexAttribPointer
解析顶点数据(每个顶点属性):
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);
函数GL.VertexAttribPointer
具有很多参数,因此让我们仔细弄清它们:
- 第一个参数指定我们要配置的顶点属性。请记住,我们在顶点着色器中使用
layout(location=0)
指定了位置顶点属性的位置。这会将顶点属性的位置设置为0,并且由于我们想将数据传递给该顶点属性,因此我们输入0。 - 下一个参数指定顶点属性的大小。顶点属性是
vec3
,因此它由3个值组成。 - 第三个参数指定数据类型,该类型为浮点型(
GLSL
中的vec*
由浮点值组成)。 - 下一个参数指定我们是否要对数据进行规范化。如果我们要输入整数数据类型(int,byte)并将其设置为
true
,则整数数据将被标准化为0(对于带符号数据,则为-1),而在转换为float
时则被标准化为1。这与我们无关,因此我们将其保留为false
。 - 第五个参数称为步幅,它告诉我们连续顶点属性之间的间隔。由于下一组位置数据的位置恰好是浮动量的3倍,因此我们将该值指定为跨度。请注意,由于我们知道数组是紧密堆积的(下一个顶点属性值之间没有空格),我们也可以将步幅指定为0,以让
OpenGL
确定步幅(这仅在值紧密堆积时有效)。每当我们拥有更多顶点属性时,我们都必须仔细定义每个顶点属性之间的间距,但稍后我们将看到更多有关该顶点的示例。 - 最后一个参数是位置数据在缓冲区中开始位置的偏移量。由于位置数据位于数据数组的开头,因此该值仅为0。我们稍后将详细探讨此参数。
每个顶点属性从VBO
管理的内存中获取数据,而从哪个VBO
获取数据(可以有多个VBO
)则取决于当前调用GL.VertexAttribPointer
时绑定到ArrayBuffer
的VBO
。 由于先前定义的VBO
在调用GL.VertexAttribPointer
之前仍然绑定,所以现在将顶点属性0与它的顶点数据关联。
现在,我们指定了OpenGL
应该如何解析顶点数据,我们还应该使用GL.EnableVertexAttribArray
启用顶点属性,并以顶点属性位置作为参数。 默认情况下,禁用顶点属性。
从那时起,我们就完成了所有设置:我们使用顶点缓冲区对象在缓冲区中初始化了顶点数据,设置了顶点和片段着色器,并告诉OpenGL
如何将顶点数据链接到顶点着色器的顶点属性。 现在在OpenGL
中绘制一个对象看起来像这样:
GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);shader.Use()
// 3. now draw the object
someOpenGLFunctionThatDrawsOurTriangle();
每次我们要绘制对象时,我们都必须重复此过程。 它看起来可能不是那么多,但是请想象一下,如果我们具有五个或更多的顶点属性,也许还有数百个不同的对象(这并不罕见)。 绑定适当的缓冲区对象并为每个对象快速配置所有顶点属性变得很麻烦。 如果可以通过某种方式将所有这些状态配置存储到一个对象中并简单地绑定该对象以恢复其状态该怎么办?
顶点数组对象
可以像顶点缓冲区对象一样绑定顶点数组对象(也称为VAO
),并且从该点开始的任何后续顶点属性调用都将存储在VAO
内。 这样做的好处是,在配置顶点属性指针时,您只需进行一次调用,并且每当我们要绘制对象时,我们都可以绑定相应的VAO
。 这使得在不同的顶点数据和属性配置之间切换就像绑定不同的VAO
一样容易。 我们刚刚设置的所有状态都存储在VAO
中。
核心OpenG
L要求我们使用VAO
,以便它知道如何处理我们的顶点输入。 如果我们无法绑定VAO
,则OpenGL
很可能会拒绝绘制任何内容。
顶点数组对象存储以下内容:
- 调用
GL.EnableVertexAttribArray
或GL.DisableVertexAttribArray
。 - 通过
GL.VertexAttribPointer
进行顶点属性配置。 - 通过调用
GL.VertexAttribPointer
与顶点属性关联的顶点缓冲区对象。
生成VAO
的过程与VBO
相似。 添加一个新的属性:
int VertexArrayObject;
然后,在OnLoad
中,在调用GL.BindVertexArray
之前,添加:
VertexArrayObject = GL.GenVertexArray();
要使用VAO
,您要做的就是使用GL.BindVertexArray
绑定VAO
。 从那时起,我们应该绑定/配置相应的VBO
和属性指针,然后取消绑定VAO
以供以后使用。 想要绘制对象时,我们只需在绘制对象之前将VAO
与首选设置绑定即可。 在代码中,这看起来像这样:
// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
GL.BindVertexArray(VertexArrayObject);
// 2. copy our vertices array in a buffer for OpenGL to use
GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);
// 3. then set our vertex attributes pointers
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
GL.EnableVertexAttribArray(0);
然后,要实际绘制对象,请将以下内容放入渲染循环中:
GL.UseProgram();
GL.BindVertexArray(VertexArrayObject);
someOpenGLFunctionThatDrawsOurTriangle();
就是这样! 到目前为止,我们完成了前几百万页的所有操作,一个VAO
存储了我们的顶点属性配置以及要使用的VBO
。 通常,当您有多个要绘制的对象时,首先要生成/配置所有VAO
(并因此生成所需的VBO
和属性指针)并将其存储以备后用。 当我们想要绘制一个对象时,我们获取相应的VAO
,将其绑定,然后绘制该对象并再次取消绑定VAO
。
为了绘制我们选择的对象,OpenGL
为我们提供了GL.DrawArrays
函数,该函数使用当前活动的着色器,先前定义的顶点属性配置以及VBO
的顶点数据(通过VAO
间接绑定)绘制基元。
shader.Use();
GL.BindVertexArray(VertexArrayObject);
GL.DrawArrays(PrimitiveType.Triangles, 0, 3);
GL.DrawArrays
函数将我们要绘制的OpenGL
基本类型作为其第一个参数。 由于我一开始就说过要绘制一个三角形,而且我不喜欢对你说谎,所以我们传入了PrimitiveType.Triangles
。 第二个参数指定我们要绘制的顶点数组的起始索引; 因为我们要绘制所有顶点,所以我们将其保留为0。最后一个参数指定要绘制多少个顶点,即3(从数据中渲染1个三角形,恰好是3个顶点)。
现在尝试编译代码,如果出现任何错误,请向后处理。 一旦您的应用程序编译,您应该看到以下结果:
完整程序的源代码可以在这里找到。
如果您的输出看起来不一样,则说明您在执行过程中可能出错了,因此请检查完整的源代码,看看是否有任何遗漏或在我们的官方Discord
服务器中询问。
增编:动态检索着色器布局
对于此示例,当我们调用GL.VertexAttribPointer
时,我们将变量的位置使用0的硬编码布局。 这仅是有效的,因为我们在shader.vert
中的输入变量将布局显式设置为0。但是,如果您不想那样做,该怎么办? 如果愿意,可以在运行时检索位置。
如果要执行此操作,请将以下函数添加到您的Shader
类中。
public int GetAttribLocation(string attribName)
{return GL.GetAttribLocation(Handle, attribName);
}
然后,在设置VAO
时,可以使用shader.GetAttribLocation("aPosition")
代替0。如果这样做,则不必再着色器中使用layout(location=0)
这一行了。
本教程将坚持使用硬编码的值,但是重要的是要知道如何使用这两种方法。
C# OpenTK教程 - 1.2 你好三角形相关推荐
- C# OpenTK教程 - 目录
更新中 起步 1.0 了解OpenGL 1.1 创建窗口 1.2 你好三角形 1.3 元素缓冲区对象 1.4 纹理 1.5 多个纹理 1.6 转换 1.7 坐标系统 1.8 相机 照明 2.1 颜色 ...
- OpenGL从入门到精通--你好三角形
三角形 github源码仓库 opengl环境准备 opengl编程从入门到精通-hello,window OpenGL从入门到精通–你好三角形 OpenGL从入门到精通–着色器的使用 绘图中需要牢记 ...
- opengl教程2,你好顶点
说明:本文翻译自http://ogldev.atspace.co.uk/www/tutorial02/tutorial02.html 我们只在本教程和下一篇教程使用了固定函数管道而不是可编程管道.实际 ...
- C# OpenTK教程 - 1.1 创建窗口
欢迎访问LearnOpenTK教程的第一个项目.本指南将教你如何从NuGet获取OpenTK,并打开您的第一个窗口.我们承诺这将是一个短暂的. 从NuGet安装 OpenTK已经发布到.NET官方的包 ...
- C++自学教程第一课——你好世界,我是柠檬鲸。
C++系列教程现在在自己学校的一个博客平台发布,几个朋友一起搭建的 [C++基础教程系列](https://blog.ytmaxoj.org/cpp_basic_liuary-0/) 下面是原来的正文 ...
- canvas系列教程01——直线、三角形、多边形、矩形、调色板
绘图步骤 html中添加 canvas 标签,通常需指定 id width(默认 300px) height(默认 150px) <canvas id="canvas" wi ...
- 现代OpenGL系列教程(一)---旋转的三角形
[写在前面] 本章主要内容: 1.基本的矩阵变换 2.基本的OpenGL Buffer Object 3.基本的GLSL(OpenGL着色语言) [正文开始] 在正式开始学习之前,我必须要说明的是: ...
- 各学科、各专业、全系列软件图文、视频安装详细教程总贴——CM(changeMax)独家制作,汇总
各学科.各专业.全系列软件图文.视频安装详细教程总贴 你好,我是change max,本人写博客已有一个年头了.经过一年多的沉淀,我发现了一个现状: 对于各种专业性的技术贴各位的关注度不高,或者说,本 ...
- 【一步步学OpenGL 31】 -《PN(Point-Normal)三角形曲面细分》
教程 31 PN(Point-Normal)三角形曲面细分 原文: http://ogldev.atspace.co.uk/www/tutorial31/tutorial31.html CSDN完整版 ...
最新文章
- android wear 上网,Android Wear 2.0智能回复不需联网?这是最新的离线AI技术
- Android作业(Activitiy)
- 缓冲流、转换流、序列化流
- 23.2.3 高速缓存依赖性(1)
- 防止Ddos攻击脚本
- 98.验证二叉搜索树
- Neo4j配置安装与测试
- Zephry_GPIO的中断使用详解以及中断原理
- Gridlayout
- 去除面部黑色素小妙招_去除脸部黑色素7个小方法,值得收藏
- toolchain安装教程支持_网上现成toolchain安装操作
- SAP ABAP 调用 BAPI_GOODSMVT_CREATE 没有执行 MIGO/MB0A 相同检查的问题
- HTML背景墙上加图标,背景墙挂件—背景墙上放一些什么挂件好
- java高清无损图片压缩
- 使用SSH完成linux和windows之间的文件互传(linux端)
- 上海睿昂生物获得数亿元战略融资,浙江大健康产业基金领投
- oracle 唯一约束 和 唯一索引
- 2553. 【NOIP2011模拟9.7】射命丸文 (StandardIO)
- 一个互联网app的开发设计(技术选型和架构)
- 英国电信集团、西班牙电话公司和澳洲电讯参与区块链试验
热门文章
- 《Thinking in java》 读了个开头
- 查询无序列表中第K小元素
- Regex.Match 方法
- 每日一课(11/75)CPU资源和存储器 之 80x86 内存管理
- css3 中background的新增加的属性的用法(一)
- 支持向量机中到底什么是支持向量
- Python 输入一些数,统计最大值及其出现的频率,求一个数的全部质因数
- oraclr 和mysql的不同_Mysql和Oracle中的不同
- mysql数据库插入datetime_往MySQL数据库datetime类型字段中插入数据库的当前时间
- linux查看程序写文件内容,Linux 文件内容查看命令