[{"data":1,"prerenderedAt":2700},["ShallowReactive",2],{"page-\u002Fcpp\u002Fopengel\u002F三角形绘制":3},{"id":4,"title":5,"body":6,"description":12,"extension":2694,"meta":2695,"navigation":835,"path":2696,"seo":2697,"stem":2698,"__hash__":2699},"content\u002Fcpp\u002Fopengel\u002F三角形绘制.md","三角形绘制",{"type":7,"value":8,"toc":2677},"minimark",[9,13,26,29,45,56,68,79,93,96,101,106,109,128,135,139,153,162,165,199,204,220,224,231,261,280,286,305,338,356,375,386,401,413,420,431,438,449,452,463,475,515,530,545,551,554,580,591,605,620,627,637,647,656,678,681,692,702,712,803,806,813,816,859,862,892,928,965,991,994,997,1004,1039,1042,1049,1064,1075,1082,1097,1106,1169,1172,1179,1188,1223,1240,1250,1275,1278,1281,1291,1298,1301,1316,1323,1343,1346,1357,1386,1393,1402,1405,1408,1423,1430,1433,1443,1446,1451,1465,1472,1487,1521,1531,1539,1547,1559,1573,1576,1662,1677,1692,1745,1748,1751,1765,1778,1781,1792,1797,1800,1815,1834,1922,1936,1939,1949,1967,1979,1982,1987,2673],[10,11,12],"p",{},"在学习此节之前，建议将这三个单词先记下来：",[14,15,16,20,23],"ul",{},[17,18,19],"li",{},"顶点数组对象：Vertex Array Object，VAO",[17,21,22],{},"顶点缓冲对象：Vertex Buffer Object，VBO",[17,24,25],{},"元素缓冲对象：Element Buffer Object，EBO 或 索引缓冲对象 Index Buffer Object，IBO",[10,27,28],{},"当指代这三个东西的时候，可能使用的是全称，也可能用的是英文缩写，翻译的时候和原文保持的一致。由于没有英文那样的分词间隔，中文全称的部分可能不太容易注意。但请记住，缩写和中文全称指代的是一个东西。",[10,30,31,32,36,37,40,41,44],{},"​\t在OpenGL中，",[33,34,35],"strong",{},"任何事物都在3D空间中","，而",[33,38,39],{},"屏幕和窗口却是2D像素数组，这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素","。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线（Graphics Pipeline，大多译为",[33,42,43],{},"管线","，实际上指的是一堆原始图形数据途经一个输送管道，期间经过各种变化处理最终出现在屏幕的过程）管理的。",[10,46,47,48,51,52,55],{},"​\t图形渲染管线可以被划分为两个主要部分：",[33,49,50],{},"第一部分把你的3D坐标转换为2D坐标","，",[33,53,54],{},"第二部分是把2D坐标转变为实际的有颜色的像素","。这个教程里，我们会简单地讨论一下图形渲染管线，以及如何利用它创建一些漂亮的像素。",[57,58,59,65],"blockquote",{},[10,60,61],{},[62,63,64],"span",{},"!NOTE",[10,66,67],{},"2D坐标和像素也是不同的，2D坐标精确表示一个点在2D空间中的位置，而2D像素是这个点的近似值，2D像素受到你的屏幕\u002F窗口分辨率的限制。",[10,69,70,71,74,75,78],{},"​\t图形渲染管线",[33,72,73],{},"接受一组3D坐标，然后把它们转变为你屏幕上的有色2D像素输出","。图形渲染管线可以被划分为几个阶段，每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的（它们都有一个特定的函数），并且很容易并行执行。正是由于它们具有并行执行的特性，当今大多数显卡都有成千上万的小处理核心，",[33,76,77],{},"它们在GPU上为每一个（渲染管线）阶段运行各自的小程序，从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)","。",[10,80,81,82,85,86,78],{},"​\t",[33,83,84],{},"每一个 GPU 的流处理器（Streaming Multiprocessor, SM，或 Compute Unit, CU）都包含"," 专门用于执行",[33,87,88,89,92],{},"着色器（Shader）",[33,90,91],{},"程序的","硬件单元",[10,94,95],{},"不过，在现代 GPU 术语中，我们需要区分几个概念：",[97,98,100],"h2",{"id":99},"流处理器smcu和着色器核心","流处理器（SM\u002FCU）和着色器核心",[102,103,105],"h3",{"id":104},"_1-流处理器-streaming-multiprocessor-sm","1. 流处理器 (Streaming Multiprocessor, SM)",[10,107,108],{},"流处理器是现代 GPU 的基本计算单元。",[14,110,111,120],{},[17,112,113,116,117,78],{},[33,114,115],{},"NVIDIA"," 将其称为 ",[33,118,119],{},"SM（Streaming Multiprocessor，流式多处理器）",[17,121,122,116,125,78],{},[33,123,124],{},"AMD",[33,126,127],{},"CU（Compute Unit，计算单元）",[10,129,130,131,134],{},"一个现代 GPU 芯片（例如 NVIDIA RTX 4090 或 AMD Radeon RX 7900 XT）包含",[33,132,133],{},"数百个","这样的 SM 或 CU。它们是 GPU 执行所有并行计算任务的“引擎”。",[102,136,138],{"id":137},"_2-着色器核心-shader-cores","2. 着色器核心 (Shader Cores)",[10,140,141,142,145,146,149,150,78],{},"着色器核心，在 NVIDIA 架构中通常被称为 ",[33,143,144],{},"CUDA 核心","，在 AMD 架构中被称为 ",[33,147,148],{},"ALU（Arithmetic Logic Unit，算术逻辑单元）"," 或 ",[33,151,152],{},"流处理器核心",[10,154,155,158,159],{},[33,156,157],{},"关键点："," ",[33,160,161],{},"这些核心就位于每一个流处理器（SM\u002FCU）的内部。",[10,163,164],{},"一个 SM\u002FCU 是一个复杂的单元，它包含：",[14,166,167,173,179,185],{},[17,168,169,172],{},[33,170,171],{},"大量的着色器核心\u002FALU："," 负责执行着色器程序中的实际数学运算（加法、乘法、三角函数等）。",[17,174,175,178],{},[33,176,177],{},"缓存 (Cache)："," 存储数据，减少内存访问延迟。",[17,180,181,184],{},[33,182,183],{},"共享内存 (Shared Memory)："," 供该 SM\u002FCU 内的核心之间快速通信。",[17,186,187,190,191,194,195,198],{},[33,188,189],{},"特殊功能单元："," 例如用于光线追踪的 ",[33,192,193],{},"RT Cores"," 或用于 AI\u002F矩阵运算的 ",[33,196,197],{},"Tensor Cores","（在 NVIDIA 架构中）。",[10,200,201],{},[33,202,203],{},"因此，你可以理解为：",[57,205,206],{},[10,207,208,211,212,215,216,219],{},[33,209,210],{},"流处理器 (SM\u002FCU)"," 是一个管理和调度工作的容器，而它内部的 ",[33,213,214],{},"着色器核心\u002FALU"," 才是真正执行你所编写的",[33,217,218],{},"顶点着色器、片段着色器","等程序的硬件。",[97,221,223],{"id":222},"现代-gpu-中的着色器概念","现代 GPU 中的“着色器”概念",[10,225,226,227,230],{},"​\t在现代 GPU 中，“着色器”的概念已经超越了单纯的颜色计算，它指的是任何可以在 ",[33,228,229],{},"GPU 上并行执行的小程序","。当你听到 GPU 硬件具备“着色器”能力时，指的是它能够高效执行以下所有类型的程序：",[232,233,234,240,249,255],"ol",{},[17,235,236,239],{},[33,237,238],{},"顶点着色器 (Vertex Shader)："," 处理 3D 几何体的变换、位置和投影。",[17,241,242,245,246,78],{},[33,243,244],{},"片段着色器 (Fragment Shader)："," 计算",[33,247,248],{},"每个像素的最终颜色（包括光照和纹理）",[17,250,251,254],{},[33,252,253],{},"几何着色器 (Geometry Shader)："," 可选，用于在 GPU 上创建新的几何体。",[17,256,257,260],{},[33,258,259],{},"计算着色器 (Compute Shader)："," 用于通用目的的并行计算任务，与图形渲染无关。",[10,262,263,264,267,268,271,272,275,276,279],{},"​\t有些着色器",[33,265,266],{},"可以由开发者配置","，因为",[33,269,270],{},"允许用自己写的着色器来代替默认的","，所以",[33,273,274],{},"能够更细致地控制图形渲染管线中的特定部分了","。因为它们运行在GPU上，所以节省了宝贵的CPU时间。OpenGL着色器是用OpenGL",[33,277,278],{},"着色器语言","(OpenGL Shading Language, GLSL)写成的，在下一节中我们再花更多时间研究它。",[10,281,282],{},[283,284],"img",{"alt":283,"src":285},".\u002Fassets\u002Fpipeline.png",[10,287,288,289,292,293,296,297,300,301,304],{},"​\t首先，我们以数组的形式",[33,290,291],{},"传递3个3D坐标作为图形渲染管线的输入","，用来",[33,294,295],{},"表示一个三角形","，这个",[33,298,299],{},"数组叫做顶点数据(Vertex Data)","；顶点数据是一系列顶点的集合。",[33,302,303],{},"一个顶点(Vertex)是一个3D坐标的数据的集合","。而这样一个顶点的数据是用顶点属性(Vertex Attribute)表示的，它可以包含任何我们想用的数据，但是简单起见，我们还是假定每个顶点只由一个3D位置(译注1)和一些颜色值组成的吧。",[57,306,307,311],{},[10,308,309],{},[62,310,64],{},[10,312,313,314,317,318,321,322,325,326,329,330,333,334,337],{},"为了让OpenGL知道我们的",[33,315,316],{},"坐标和颜色值构成的到底是什么","，OpenGL需要你去",[33,319,320],{},"指定这些数据所表示的渲染类型","。我们是希望把这些",[33,323,324],{},"数据渲染成一系列的点","？一系列的",[33,327,328],{},"三角形","？还是仅仅是一个长长的线？做出的这些",[33,331,332],{},"提示叫做图元(Primitive)","，任何一个绘制指令的调用都将",[33,335,336],{},"把图元传递给OpenGL","。这是其中的几个：GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。",[10,339,340,341,344,345,348,349,352,353,78],{},"​\t图形渲染管线的第一个部分是",[33,342,343],{},"顶点着色器(Vertex Shader)","，它把一个",[33,346,347],{},"单独的顶点作为输入","。顶点着色器主要的目的是",[33,350,351],{},"把3D坐标转为另一种3D坐标","（后面会解释），同时顶点着色器允许我们",[33,354,355],{},"对顶点属性进行一些基本处理",[10,357,358,359,362,363,366,367,370,371,374],{},"​\t顶点着色器阶段的输出",[33,360,361],{},"可以选择性地传递给几何着色器(Geometry Shader)","。几何着色器将",[33,364,365],{},"一组顶点作为输入","，这些顶点",[33,368,369],{},"形成图元","，并且能够",[33,372,373],{},"通过发出新的顶点来形成新的","(或其他)图元来生成其他形状。在这个例子中，它从给定的形状中生成第二个三角形。",[10,376,377,378,381,382,385],{},"​\t图元装配(Primitive Assembly)阶段将顶点着色器（或几何着色器）",[33,379,380],{},"输出的所有顶点作为输入","（如果是GL_POINTS，那么就是一个顶点），并将",[33,383,384],{},"所有的点装配成指定图元的形状","；本节例子中是两个三角形。",[10,387,388,389,392,393,396,397,400],{},"​\t图元装配阶段的输出会被",[33,390,391],{},"传入光栅化阶段(Rasterization Stage)","，这里",[33,394,395],{},"它会把图元映射为最终屏幕上相应的像素","，生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在",[33,398,399],{},"片段着色器运行之前会执行裁切(Clipping)","。裁切会丢弃超出你的视图以外的所有像素，用来提升执行效率。",[57,402,403,407],{},[10,404,405],{},[62,406,64],{},[10,408,409,410,78],{},"OpenGL中的一个片段是",[33,411,412],{},"OpenGL渲染一个像素所需的所有数据",[10,414,415,416,419],{},"​\t片段着色器的主要目的是",[33,417,418],{},"计算一个像素的最终颜色，这也是所有OpenGL高级效果产生的地方","。通常，片段着色器包含3D场景的数据（比如光照、阴影、光的颜色等等），这些数据可以被用来计算最终像素的颜色。",[10,421,422,423,426,427,430],{},"​\t在所有对应颜色值确定以后，",[33,424,425],{},"最终的对象将会被传到最后一个阶段，我们叫做Alpha测试和混合(Blending)阶段","。这个阶段检测片段的对应的深度（和模板(Stencil)）值（后面会讲），用它们来判断这个像素是其它物体的前面还是后面，决定是否应该丢弃。这个阶段也会检查alpha值（alpha值定义了一个物体的透明度）",[33,428,429],{},"并对物体进行混合(Blend)","。所以，即使在片段着色器中计算出来了一个像素输出的颜色，在渲染多个三角形的时候最后的像素颜色也可能完全不同。",[10,432,433,434,437],{},"​\t可以看到，图形渲染管线非常复杂，它包含很多可配置的部分。然而，对于大多数场合，我们只需要",[33,435,436],{},"配置顶点和片段着色器","就行了。几何着色器是可选的，通常使用它默认的着色器就行了。",[10,439,440,441,444,445,448],{},"​\t在现代OpenGL中，我们",[33,442,443],{},"必须","定义至少一个",[33,446,447],{},"顶点着色器和一个片段着色器","（因为GPU中没有默认的顶点\u002F片段着色器）。出于这个原因，刚开始学习现代OpenGL的时候可能会非常困难，因为在你能够渲染自己的第一个三角形之前已经需要了解一大堆知识了。在本节结束你最终渲染出你的三角形的时候，你也会了解到非常多的图形编程知识。",[97,450,451],{"id":451},"顶点输入",[10,453,454,455,458,459,462],{},"​\t开始绘制图形之前，我们需要先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库，所以在OpenGL中我们指定的所有坐标都是3D坐标（x、y和z）。OpenGL不是简单地把",[33,456,457],{},"所有的","3D坐标变换为屏幕上的2D像素；",[33,460,461],{},"OpenGL仅当3D坐标在3个轴（x、y和z）上-1.0到1.0的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates)","，此范围内的坐标最终显示在屏幕上（在这个范围以外的坐标则不会显示）。",[10,464,465,466,469,470,474],{},"​\t由于我们希望渲染一个三角形，我们一共要指定三个顶点，每个顶点都有一个3D位置。",[33,467,468],{},"我们会将它们以标准化设备坐标的形式","（OpenGL的可见区域）定义为一个",[471,472,473],"code",{},"float","数组。",[476,477,482],"pre",{"className":478,"code":479,"language":480,"meta":481,"style":481},"language-c++ shiki shiki-themes github-light github-dark","float vertices[] = {\n    -0.5f, -0.5f, 0.0f,\n     0.5f, -0.5f, 0.0f,\n     0.0f,  0.5f, 0.0f\n};\n","c++","",[471,483,484,491,497,503,509],{"__ignoreMap":481},[62,485,488],{"class":486,"line":487},"line",1,[62,489,490],{},"float vertices[] = {\n",[62,492,494],{"class":486,"line":493},2,[62,495,496],{},"    -0.5f, -0.5f, 0.0f,\n",[62,498,500],{"class":486,"line":499},3,[62,501,502],{},"     0.5f, -0.5f, 0.0f,\n",[62,504,506],{"class":486,"line":505},4,[62,507,508],{},"     0.0f,  0.5f, 0.0f\n",[62,510,512],{"class":486,"line":511},5,[62,513,514],{},"};\n",[10,516,517,518,521,522,526,527,78],{},"​\t由于OpenGL是在3D空间中工作的，而我们渲染的是一个2D三角形，",[33,519,520],{},"我们将它顶点的z坐标设置为0.0","。这样子的话三角形每一点的",[523,524,525],"em",{},"深度","(Depth，译注2)都是一样的，",[33,528,529],{},"从而使它看上去像是2D的",[10,531,532,533,536,537,540,541,544],{},"​\t一旦你的",[33,534,535],{},"顶点坐标已经在顶点着色器中处理过","，它们就应该是",[33,538,539],{},"标准化设备坐标","了，标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。",[33,542,543],{},"任何落在范围外的坐标都会被丢弃\u002F裁剪，不会显示在你的屏幕上","。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴)：",[10,546,547],{},[283,548],{"alt":549,"src":550},"image-20251005141205095",".\u002Fassets\u002Fimage-20251005141205095.png",[10,552,553],{},"​\t与通常的屏幕坐标不同，y轴正方向为向上，(0, 0)坐标是这个图像的中心，而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中，否则它们就不可见了。",[10,555,556,557,560,561,564,565,568,569,572,573,576,577,78],{},"​\t通过使用由glViewport函数提供的数据，",[33,558,559],{},"进行视口变换(Viewport Transform)","，标准化设备坐标(Normalized Device Coordinates)会",[33,562,563],{},"变换为屏幕空间坐标(Screen-space Coordinates)","。所得的",[33,566,567],{},"屏幕空间坐标又会被变换为片段输入到片段着色器中","。 定义这样的顶点数据以后，我们会把它作为输入发送给图形渲染管线的第一个处理阶段：顶点着色器。",[33,570,571],{},"它会在GPU上创建内存用于储存我们的顶点数据","，还要",[33,574,575],{},"配置OpenGL如何解释这些内存","，并且指定其如何发送给显卡。",[33,578,579],{},"顶点着色器接着会处理我们在内存中指定数量的顶点",[10,581,582,583,586,587,590],{},"​\t我们通过",[33,584,585],{},"顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存","，它会在GPU内存（通常被称为显存）中储存大量顶点。使用这些缓冲对象的好处是",[33,588,589],{},"我们可以一次性的发送一大批数据到显卡上","，而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢，所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后，顶点着色器几乎能立即访问顶点，这是个非常快的过程。",[10,592,593,594,600,601,604],{},"​\t顶点缓冲对象是我们在[OpenGL](",[595,596,597],"a",{"href":597,"rel":598},"https:\u002F\u002Flearnopengl-cn.github.io\u002F01",[599],"nofollow"," Getting started\u002F01 OpenGL\u002F)教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样，",[33,602,603],{},"这个缓冲有一个独一无二的ID，所以我们可以使用glGenBuffers函数生成一个带有缓冲ID的VBO对象","：",[476,606,608],{"className":478,"code":607,"language":480,"meta":481,"style":481},"unsigned int VBO;\nglGenBuffers(1, &VBO);\n",[471,609,610,615],{"__ignoreMap":481},[62,611,612],{"class":486,"line":487},[62,613,614],{},"unsigned int VBO;\n",[62,616,617],{"class":486,"line":493},[62,618,619],{},"glGenBuffers(1, &VBO);\n",[10,621,622,623,626],{},"​\tOpenGL有很多缓冲对象类型，",[33,624,625],{},"顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER","。OpenGL允许我们同时绑定多个缓冲，只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上：",[476,628,630],{"className":478,"code":629,"language":480,"meta":481,"style":481},"glBindBuffer(GL_ARRAY_BUFFER, VBO);  \n",[471,631,632],{"__ignoreMap":481},[62,633,634],{"class":486,"line":487},[62,635,636],{},"glBindBuffer(GL_ARRAY_BUFFER, VBO);\n",[10,638,639,640,643,644,604],{},"​\t从这一刻起，我们使用的任何（在GL_ARRAY_BUFFER目标上的）",[33,641,642],{},"缓冲调用都会用来配置当前绑定的缓冲(VBO)","。然后我们可以调用glBufferData函数，",[33,645,646],{},"它会把之前定义的顶点数据复制到缓冲的内存中",[476,648,650],{"className":478,"code":649,"language":480,"meta":481,"style":481},"glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n",[471,651,652],{"__ignoreMap":481},[62,653,654],{"class":486,"line":487},[62,655,649],{},[10,657,658,659,662,663,666,667,670,671,674,675,78],{},"​\tglBufferData是一个",[33,660,661],{},"专门用来把用户定义的数据复制到当前绑定缓冲的函数","。它的第一个参数是",[33,664,665],{},"目标缓冲的类型","：顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。",[33,668,669],{},"第二个参数指定传输数据的大小(以字节为单位)","；用一个简单的",[471,672,673],{},"sizeof","计算出顶点数据大小就行。",[33,676,677],{},"第三个参数是我们希望发送的实际数据",[10,679,680],{},"第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式：",[14,682,683,686,689],{},[17,684,685],{},"GL_STATIC_DRAW ：数据不会或几乎不会改变。",[17,687,688],{},"GL_DYNAMIC_DRAW：数据会被改变很多。",[17,690,691],{},"GL_STREAM_DRAW ：数据每次绘制时都会改变。",[10,693,694,695,698,699],{},"​\t三角形的位置数据不会改变，",[33,696,697],{},"每次渲染调用时都保持原样，所以它的使用类型最好是GL_STATIC_DRAW","。如果，比如说一个缓冲中的数据将频繁被改变，那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW，这样就",[33,700,701],{},"能确保显卡把数据放在能够高速写入的内存部分。",[10,703,704,705,149,708,711],{},"​\t当你调用 ",[471,706,707],{},"glBufferData",[471,709,710],{},"glBufferStorage"," 时，你需要传入这个提示：",[713,714,715,731],"table",{},[716,717,718],"thead",{},[719,720,721,725,728],"tr",{},[722,723,724],"th",{},"提示符",[722,726,727],{},"含义",[722,729,730],{},"驱动程序策略",[732,733,734,757,780],"tbody",{},[719,735,736,744,750],{},[737,738,739],"td",{},[33,740,741],{},[471,742,743],{},"GL_STATIC_DRAW",[737,745,746,747,78],{},"数据只设置一次，然后绘制",[33,748,749],{},"很多次",[737,751,752,753,756],{},"驱动程序会将数据放入 ",[33,754,755],{},"GPU 的只读\u002F专用显存（VRAM）"," 中。访问速度最快，但从 CPU 更新（写入）会非常慢。",[719,758,759,766,769],{},[737,760,761],{},[33,762,763],{},[471,764,765],{},"GL_DYNAMIC_DRAW",[737,767,768],{},"数据会频繁更改（例如每帧更新一次，但可能不会完全替换），然后绘制很多次。",[737,770,771,772,775,776,779],{},"驱动程序可能会将数据放入 ",[33,773,774],{},"GPU 内存池中易于更新"," 的部分，例如 ",[33,777,778],{},"系统内存（RAM）"," 中易于 DMA（直接内存访问）访问的区域，或者 VRAM 中可快速写入的区域。",[719,781,782,789,796],{},[737,783,784],{},[33,785,786],{},[471,787,788],{},"GL_STREAM_DRAW",[737,790,791,792,795],{},"数据只设置一次，绘制",[33,793,794],{},"一两次","，然后丢弃。",[737,797,798,799,802],{},"驱动程序可能会使用一种 ",[33,800,801],{},"“环形缓冲（Ring Buffer）”"," 策略。数据被写入一个临时、高速的区域，快速传输给 GPU，然后在下一帧中被新的数据覆盖。追求极致的单次传输速度。",[97,804,805],{"id":805},"顶点着色器",[10,807,808,809,812],{},"​\t顶点着色器(Vertex Shader)是几个可编程着色器中的一个。如果我们打算做渲染的话，",[33,810,811],{},"现代OpenGL需要我们至少设置一个顶点和一个片段着色器","。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下一节中我们会更详细的讨论着色器。",[10,814,815],{},"​\t我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器，然后编译这个着色器，这样我们就可以在程序中使用它了。下面你会看到一个非常基础的GLSL顶点着色器的源代码：",[476,817,819],{"className":478,"code":818,"language":480,"meta":481,"style":481},"#version 330 core\nlayout (location = 0) in vec3 aPos;\n\nvoid main()\n{\n    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n}\n",[471,820,821,826,831,837,842,847,853],{"__ignoreMap":481},[62,822,823],{"class":486,"line":487},[62,824,825],{},"#version 330 core\n",[62,827,828],{"class":486,"line":493},[62,829,830],{},"layout (location = 0) in vec3 aPos;\n",[62,832,833],{"class":486,"line":499},[62,834,836],{"emptyLinePlaceholder":835},true,"\n",[62,838,839],{"class":486,"line":505},[62,840,841],{},"void main()\n",[62,843,844],{"class":486,"line":511},[62,845,846],{},"{\n",[62,848,850],{"class":486,"line":849},6,[62,851,852],{},"    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n",[62,854,856],{"class":486,"line":855},7,[62,857,858],{},"}\n",[10,860,861],{},"​\t可以看到，GLSL看起来很像C语言。每个着色器都起始于一个版本声明。OpenGL 3.3以及和更高版本中，GLSL版本号和OpenGL的版本是匹配的（比如说GLSL 420版本对应于OpenGL 4.2）。我们同样明确表示我们会使用核心模式。",[10,863,864,865,868,869,872,873,876,877,880,881,883,884,887,888,891],{},"​\t下一步，使用",[471,866,867],{},"in","关键字，在",[33,870,871],{},"顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)","。现在我们只关心位置(Position)数据，所以",[33,874,875],{},"我们只需要一个顶点属性","。GLSL有一个",[33,878,879],{},"向量数据类型","，它包含1到4个",[471,882,473],{},"分量，包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标，我们就创建一个",[471,885,886],{},"vec3","输入变量aPos。我们同样也通过",[471,889,890],{},"layout (location = 0)","设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。",[57,893,894,898,903],{},[10,895,896],{},[62,897,64],{},[10,899,900],{},[33,901,902],{},"向量(Vector)",[10,904,905,906,909,910,909,913,916,917,920,921,923,924,927],{},"在图形编程中我们经常会使用向量这个数学概念，因为它简明地表达了任意空间中的位置和方向，并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量，每个分量值都代表空间中的一个坐标，它们可以通过",[471,907,908],{},"vec.x","、",[471,911,912],{},"vec.y",[471,914,915],{},"vec.z","和",[471,918,919],{},"vec.w","来获取。注意",[471,922,919],{},"分量不是用作表达空间中的位置的（我们处理的是3D不是4D），",[33,925,926],{},"而是用在所谓透视除法(Perspective Division)上","。我们会在后面的教程中更详细地讨论向量。",[10,929,930,931,934,935,938,939,942,943,946,947,950,951,953,954,956,957,960,961,964],{},"​\t为了设置",[33,932,933],{},"顶点着色器的输出","，我们必须把",[33,936,937],{},"位置数据赋值给预定义的gl_Position变量","，它在幕后是",[471,940,941],{},"vec4","类型的。在main函数的最后，",[33,944,945],{},"我们将gl_Position设置的值会成为该顶点着色器的输出","。由于我们的",[33,948,949],{},"输入是一个3分量的向量，我们必须把它转换为4分量的","。我们可以把",[471,952,886],{},"的数据作为",[471,955,941],{},"构造器的参数，同时把",[471,958,959],{},"w","分量设置为",[471,962,963],{},"1.0f","（我们会在后面解释为什么）来完成这一任务。",[57,966,967,972],{},[10,968,969],{},[62,970,971],{},"!IMPORTANT",[10,973,974,975,978,979,982,983,986,987,990],{},"在 OpenGL 的顶点着色器中，将 3 分量的位置向量转换为 4 分量的 vec4 向量，并将 w 分量设置为 1.0，正是使用了",[33,976,977],{},"齐次坐标（Homogeneous Coordinates）"," 的概念。齐次坐标是三维图形学和 OpenGL ",[33,980,981],{},"转换管线"," 的基石，它们的作用是让",[33,984,985],{},"平移（Translation）"," 这种特殊的变换能够用 ",[33,988,989],{},"矩阵乘法"," 来表示。",[10,992,993],{},"​\t当前这个顶点着色器可能是我们能想到的最简单的顶点着色器了，因为我们对输入数据什么都没有处理就把它传到着色器的输出了。在真实的程序里输入数据通常都不是标准化设备坐标，所以我们首先必须先把它们转换至OpenGL的可视区域内。",[97,995,996],{"id":996},"编译着色器",[10,998,999,1000,1003],{},"​\t现在，我们暂时将",[33,1001,1002],{},"顶点着色器的源代码硬编码在代码文件顶部的C风格字符串","中：",[476,1005,1007],{"className":478,"code":1006,"language":480,"meta":481,"style":481},"const char *vertexShaderSource = \"#version 330 core\\n\"\n    \"layout (location = 0) in vec3 aPos;\\n\"\n    \"void main()\\n\"\n    \"{\\n\"\n    \"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n\"\n    \"}\\0\";\n",[471,1008,1009,1014,1019,1024,1029,1034],{"__ignoreMap":481},[62,1010,1011],{"class":486,"line":487},[62,1012,1013],{},"const char *vertexShaderSource = \"#version 330 core\\n\"\n",[62,1015,1016],{"class":486,"line":493},[62,1017,1018],{},"    \"layout (location = 0) in vec3 aPos;\\n\"\n",[62,1020,1021],{"class":486,"line":499},[62,1022,1023],{},"    \"void main()\\n\"\n",[62,1025,1026],{"class":486,"line":505},[62,1027,1028],{},"    \"{\\n\"\n",[62,1030,1031],{"class":486,"line":511},[62,1032,1033],{},"    \"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n\"\n",[62,1035,1036],{"class":486,"line":849},[62,1037,1038],{},"    \"}\\0\";\n",[10,1040,1041],{},"为了能够让OpenGL使用它，我们必须在运行时动态编译它的源代码。",[10,1043,1044,1045,1048],{},"​\t我们首先要做的是创建一个着色器对象，注意还是用ID来引用的。所以我们储存这个顶点着色器为",[471,1046,1047],{},"unsigned int","，然后用glCreateShader创建这个着色器：",[476,1050,1052],{"className":478,"code":1051,"language":480,"meta":481,"style":481},"unsigned int vertexShader;\nvertexShader = glCreateShader(GL_VERTEX_SHADER);\n",[471,1053,1054,1059],{"__ignoreMap":481},[62,1055,1056],{"class":486,"line":487},[62,1057,1058],{},"unsigned int vertexShader;\n",[62,1060,1061],{"class":486,"line":493},[62,1062,1063],{},"vertexShader = glCreateShader(GL_VERTEX_SHADER);\n",[10,1065,1066,1067,1070,1071,1074],{},"​\t我们把需要",[33,1068,1069],{},"创建的着色器类型以参数形式提供给glCreateShader","。由于我们正在",[33,1072,1073],{},"创建一个顶点着色器","，传递的参数是GL_VERTEX_SHADER。",[10,1076,1077,1078,1081],{},"​\t下一步我们把这个",[33,1079,1080],{},"着色器源码附加到着色器对象上","，然后编译它：",[476,1083,1085],{"className":478,"code":1084,"language":480,"meta":481,"style":481},"glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\nglCompileShader(vertexShader);\n",[471,1086,1087,1092],{"__ignoreMap":481},[62,1088,1089],{"class":486,"line":487},[62,1090,1091],{},"glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\n",[62,1093,1094],{"class":486,"line":493},[62,1095,1096],{},"glCompileShader(vertexShader);\n",[10,1098,81,1099,1102,1103,78],{},[33,1100,1101],{},"glShaderSource函数把要编译的着色器对象作为第一个参数","。第二参数指定了传递的源码字符串数量，这里只有一个。第三个参数是顶点着色器真正的源码，第四个参数我们先设置为",[471,1104,1105],{},"NULL",[57,1107,1108,1112,1115,1135,1138,1166],{},[10,1109,1110],{},[62,1111,64],{},[10,1113,1114],{},"你可能会希望检测在调用glCompileShader后编译是否成功了，如果没成功的话，你还会希望知道错误是什么，这样你才能修复它们。检测编译时错误可以通过以下代码来实现：",[476,1116,1118],{"className":478,"code":1117,"language":480,"meta":481,"style":481},"int  success;\nchar infoLog[512];\nglGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);\n",[471,1119,1120,1125,1130],{"__ignoreMap":481},[62,1121,1122],{"class":486,"line":487},[62,1123,1124],{},"int  success;\n",[62,1126,1127],{"class":486,"line":493},[62,1128,1129],{},"char infoLog[512];\n",[62,1131,1132],{"class":486,"line":499},[62,1133,1134],{},"glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);\n",[10,1136,1137],{},"首先我们定义一个整型变量来表示是否成功编译，还定义了一个储存错误消息（如果有的话）的容器。然后我们用glGetShaderiv检查是否编译成功。如果编译失败，我们会用glGetShaderInfoLog获取错误消息，然后打印它。",[476,1139,1141],{"className":478,"code":1140,"language":480,"meta":481,"style":481},"if(!success)\n{\n    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);\n    std::cout \u003C\u003C \"ERROR::SHADER::VERTEX::COMPILATION_FAILED\\n\" \u003C\u003C infoLog \u003C\u003C std::endl;\n}\n",[471,1142,1143,1148,1152,1157,1162],{"__ignoreMap":481},[62,1144,1145],{"class":486,"line":487},[62,1146,1147],{},"if(!success)\n",[62,1149,1150],{"class":486,"line":493},[62,1151,846],{},[62,1153,1154],{"class":486,"line":499},[62,1155,1156],{},"    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);\n",[62,1158,1159],{"class":486,"line":505},[62,1160,1161],{},"    std::cout \u003C\u003C \"ERROR::SHADER::VERTEX::COMPILATION_FAILED\\n\" \u003C\u003C infoLog \u003C\u003C std::endl;\n",[62,1163,1164],{"class":486,"line":511},[62,1165,858],{},[10,1167,1168],{},"如果编译的时候没有检测到任何错误，顶点着色器就被编译成功了。",[97,1170,1171],{"id":1171},"片段着色器",[10,1173,1174,1175,1178],{},"​\t片段着色器(Fragment Shader)是",[33,1176,1177],{},"第二个也是最后一个我们打算创建的用于渲染三角形的着色器","。片段着色器所做的是计算像素最后的颜色输出。为了让事情更简单，我们的片段着色器将会一直输出橘黄色。",[57,1180,1181,1185],{},[10,1182,1183],{},[62,1184,64],{},[10,1186,1187],{},"在计算机图形中颜色被表示为有4个元素的数组：红色、绿色、蓝色和alpha(透明度)分量，通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候，我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f，绿为1.0f，我们会得到两个颜色的混合色，即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色！",[476,1189,1191],{"className":478,"code":1190,"language":480,"meta":481,"style":481},"#version 330 core\nout vec4 FragColor;\n\nvoid main()\n{\n    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n} \n",[471,1192,1193,1197,1202,1206,1210,1214,1219],{"__ignoreMap":481},[62,1194,1195],{"class":486,"line":487},[62,1196,825],{},[62,1198,1199],{"class":486,"line":493},[62,1200,1201],{},"out vec4 FragColor;\n",[62,1203,1204],{"class":486,"line":499},[62,1205,836],{"emptyLinePlaceholder":835},[62,1207,1208],{"class":486,"line":505},[62,1209,841],{},[62,1211,1212],{"class":486,"line":511},[62,1213,846],{},[62,1215,1216],{"class":486,"line":849},[62,1217,1218],{},"    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n",[62,1220,1221],{"class":486,"line":855},[62,1222,858],{},[10,1224,1225,1226,296,1229,1232,1233,1236,1237,1239],{},"​\t片段着色器",[33,1227,1228],{},"只需要一个输出变量",[33,1230,1231],{},"变量是一个4分量向量，它表示的是最终的输出颜色","，我们应该自己将其计算出来。声明输出变量可以使用",[471,1234,1235],{},"out","关键字，这里我们命名为FragColor。下面，我们将一个Alpha值为1.0(1.0代表完全不透明)的橘黄色的",[471,1238,941],{},"赋值给颜色输出。",[10,1241,1242,1243,1246,1247,604],{},"​\t编译",[33,1244,1245],{},"片段着色器的过程与顶点着色器类似","，只不过我们使用",[33,1248,1249],{},"GL_FRAGMENT_SHADER常量作为着色器类型",[476,1251,1253],{"className":478,"code":1252,"language":480,"meta":481,"style":481},"unsigned int fragmentShader;\nfragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\nglShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\nglCompileShader(fragmentShader);\n",[471,1254,1255,1260,1265,1270],{"__ignoreMap":481},[62,1256,1257],{"class":486,"line":487},[62,1258,1259],{},"unsigned int fragmentShader;\n",[62,1261,1262],{"class":486,"line":493},[62,1263,1264],{},"fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\n",[62,1266,1267],{"class":486,"line":499},[62,1268,1269],{},"glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\n",[62,1271,1272],{"class":486,"line":505},[62,1273,1274],{},"glCompileShader(fragmentShader);\n",[10,1276,1277],{},"​\t两个着色器现在都编译了，剩下的事情是把**两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)**中。",[102,1279,1280],{"id":1280},"着色器程序",[10,1282,81,1283,1286,1287,1290],{},[33,1284,1285],{},"着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本","。如果要使用刚才编译的着色器我们必须把它们",[33,1288,1289],{},"链接(Link)为一个着色器程序对象","，然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。",[10,1292,1293,1294,1297],{},"​\t当链接着色器至一个程序的时候，",[33,1295,1296],{},"它会把每个着色器的输出链接到下个着色器的输入","。当输出和输入不匹配的时候，你会得到一个连接错误。",[10,1299,1300],{},"创建一个程序对象很简单：",[476,1302,1304],{"className":478,"code":1303,"language":480,"meta":481,"style":481},"unsigned int shaderProgram;\nshaderProgram = glCreateProgram();\n",[471,1305,1306,1311],{"__ignoreMap":481},[62,1307,1308],{"class":486,"line":487},[62,1309,1310],{},"unsigned int shaderProgram;\n",[62,1312,1313],{"class":486,"line":493},[62,1314,1315],{},"shaderProgram = glCreateProgram();\n",[10,1317,1318,1319,1322],{},"​\tglCreateProgram函数创建一个程序，",[33,1320,1321],{},"并返回新创建程序对象的ID引用","。现在我们需要把之前编译的着色器附加到程序对象上，然后用glLinkProgram链接它们：",[476,1324,1326],{"className":478,"code":1325,"language":480,"meta":481,"style":481},"glAttachShader(shaderProgram, vertexShader);\nglAttachShader(shaderProgram, fragmentShader);\nglLinkProgram(shaderProgram);\n",[471,1327,1328,1333,1338],{"__ignoreMap":481},[62,1329,1330],{"class":486,"line":487},[62,1331,1332],{},"glAttachShader(shaderProgram, vertexShader);\n",[62,1334,1335],{"class":486,"line":493},[62,1336,1337],{},"glAttachShader(shaderProgram, fragmentShader);\n",[62,1339,1340],{"class":486,"line":499},[62,1341,1342],{},"glLinkProgram(shaderProgram);\n",[10,1344,1345],{},"代码应该很清楚，我们把着色器附加到了程序上，然后用glLinkProgram链接。",[10,1347,1348,1349,1352,1353,1356],{},"​\t就像着色器的编译一样，我们也可以",[33,1350,1351],{},"检测链接着色器程序是否失败","，并获取相应的日志。",[33,1354,1355],{},"与上面不同，我们不会调用glGetShaderiv和glGetShaderInfoLog","，现在我们使用：",[476,1358,1360],{"className":478,"code":1359,"language":480,"meta":481,"style":481},"glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);\nif(!success) {\n    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);\n    ...\n}\n",[471,1361,1362,1367,1372,1377,1382],{"__ignoreMap":481},[62,1363,1364],{"class":486,"line":487},[62,1365,1366],{},"glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);\n",[62,1368,1369],{"class":486,"line":493},[62,1370,1371],{},"if(!success) {\n",[62,1373,1374],{"class":486,"line":499},[62,1375,1376],{},"    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);\n",[62,1378,1379],{"class":486,"line":505},[62,1380,1381],{},"    ...\n",[62,1383,1384],{"class":486,"line":511},[62,1385,858],{},[10,1387,1388,1389,1392],{},"​\t得到的结果就是一个程序对象，我们可以",[33,1390,1391],{},"调用glUseProgram函数，用刚创建的程序对象作为它的参数","，以激活这个程序对象：",[476,1394,1396],{"className":478,"code":1395,"language":480,"meta":481,"style":481},"glUseProgram(shaderProgram);\n",[471,1397,1398],{"__ignoreMap":481},[62,1399,1400],{"class":486,"line":487},[62,1401,1395],{},[10,1403,1404],{},"在glUseProgram函数调用之后，每个着色器调用和渲染调用都会使用这个程序对象（也就是之前写的着色器)了。",[10,1406,1407],{},"​\t对了，在把着色器对象链接到程序对象以后，记得删除着色器对象，我们不再需要它们了：",[476,1409,1411],{"className":478,"code":1410,"language":480,"meta":481,"style":481},"glDeleteShader(vertexShader);\nglDeleteShader(fragmentShader);\n",[471,1412,1413,1418],{"__ignoreMap":481},[62,1414,1415],{"class":486,"line":487},[62,1416,1417],{},"glDeleteShader(vertexShader);\n",[62,1419,1420],{"class":486,"line":493},[62,1421,1422],{},"glDeleteShader(fragmentShader);\n",[10,1424,1425,1426,1429],{},"​\t现在，我们已经",[33,1427,1428],{},"把输入顶点数据发送给了GPU，并指示了GPU如何在顶点和片段着色器中处理它","。就快要完成了，但还没结束，OpenGL还不知道它该如何解释内存中的顶点数据，以及它该如何将顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。",[97,1431,1432],{"id":1432},"链接顶点属性",[10,1434,81,1435,1438,1439,1442],{},[33,1436,1437],{},"顶点着色器允许我们指定任何以顶点属性为形式的输入","。这使其具有很强的灵活性的同时，它还的确意味着",[33,1440,1441],{},"我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性","。所以，我们必须在渲染前指定OpenGL该如何解释顶点数据。",[10,1444,1445],{},"​\t我们的顶点缓冲数据会被解析为下面这样子：",[10,1447,1448],{},[283,1449],{"alt":283,"src":1450},".\u002Fassets\u002Fvertex_attribute_pointer.png",[14,1452,1453,1456,1459,1462],{},[17,1454,1455],{},"位置数据被储存为32位（4字节）浮点值。",[17,1457,1458],{},"每个位置包含3个这样的值。",[17,1460,1461],{},"在这3个值之间没有空隙（或其他值）。这几个值在数组中紧密排列(Tightly Packed)。",[17,1463,1464],{},"数据中第一个值在缓冲开始的位置。",[10,1466,1467,1468,1471],{},"​\t有了这些信息我们就可以",[33,1469,1470],{},"使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据","（应用到逐个顶点属性上）了：",[476,1473,1475],{"className":478,"code":1474,"language":480,"meta":481,"style":481},"glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\nglEnableVertexAttribArray(0);\n",[471,1476,1477,1482],{"__ignoreMap":481},[62,1478,1479],{"class":486,"line":487},[62,1480,1481],{},"glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\n",[62,1483,1484],{"class":486,"line":493},[62,1485,1486],{},"glEnableVertexAttribArray(0);\n",[10,1488,1489,1490,1493,1494,1497,1498,1500,1501,1504,1505,1508,1509,1514,1515,1517,1518,1520],{},"​\t这两行代码必须在 ",[33,1491,1492],{},"绑定 VAO"," 和 ",[33,1495,1496],{},"绑定 VBO"," 之后执行，它们的作用是建立 VBO 数据到",[33,1499,805],{},"输入变量之间的连接和解析规则。",[471,1502,1503],{},"glEnableVertexAttribArray(0)"," 解释:     这个函数的作用相对简单，它是",[33,1506,1507],{},"启用","刚才配置的顶点属性。",[33,1510,1511],{},[471,1512,1513],{},"0","：同样是指定要操作的顶点属性的索引，对应于 ",[471,1516,890],{},"。在 OpenGL 中，顶点属性默认是禁用的，你必须明确",[33,1519,1507],{},"它，否则 GPU 在渲染时不会读取这个属性的数据。",[10,1522,1523,1527,1528,1530],{},[33,1524,1525],{},[471,1526,1513],{},": 告诉 GPU 这个配置对应于顶点着色器中的 ",[471,1529,890],{}," 输入变量。",[10,1532,1533,1538],{},[33,1534,1535],{},[471,1536,1537],{},"3",": 每个顶点属性由 3 个分量组成 (即 x,y,z)。",[10,1540,1541,1546],{},[33,1542,1543],{},[471,1544,1545],{},"GL_FLOAT",": 分量的数据类型是浮点数。",[10,1548,1549,1554,1555,1558],{},[33,1550,1551],{},[471,1552,1553],{},"3 \\* sizeof(float)",": ",[33,1556,1557],{},"步长 (Stride)","。从一个顶点的 x,y,z 开始，要跳过 3×4=12 字节才能找到下一个顶点的 x,y,z。",[10,1560,1561,1554,1566,1569,1570,1572],{},[33,1562,1563],{},[471,1564,1565],{},"(void\\*)0",[33,1567,1568],{},"偏移量 (Offset)","。数据从 VBO 的起始位置 (",[471,1571,1513],{}," 字节) 开始。",[10,1574,1575],{},"glVertexAttribPointer函数：",[14,1577,1578,1598,1608,1619,1630,1648],{},[17,1579,1580,1581,1584,1585,1588,1589,1591,1592,1595,1596,78],{},"第一个参数",[33,1582,1583],{},"指定我们要配置的顶点属性","。还记得我们在顶点着色器中使用",[471,1586,1587],{},"layout(location = 0)","定义了position顶点属性的位置值(Location)吗？它可以把顶点属性的位置值设置为",[471,1590,1513],{},"。因为",[33,1593,1594],{},"我们希望把数据传递到这一个顶点属性中","，所以这里我们传入",[471,1597,1513],{},[17,1599,1600,1601,1604,1605,1607],{},"第二个参数",[33,1602,1603],{},"指定顶点属性的大小","。顶点属性是一个",[471,1606,886],{},"，它由3个值组成，所以大小是3。",[17,1609,1610,1611,1614,1615,1618],{},"第三个参数",[33,1612,1613],{},"指定数据的类型","，这里是GL_FLOAT(GLSL中",[471,1616,1617],{},"vec*","都是由浮点数值组成的)。",[17,1620,1621,1622,1625,1626,1629],{},"下个参数定义",[33,1623,1624],{},"我们是否希望数据被标准化(Normalize)","。如果我们设置为GL_TRUE，",[33,1627,1628],{},"所有数据都会被映射到0","（对于有符号型signed数据是-1）到1之间。我们把它设置为GL_FALSE。",[17,1631,1632,1633,1636,1637,1639,1640,1643,1644,1647],{},"第五个参数叫做步长(Stride)，",[33,1634,1635],{},"它告诉我们在连续的顶点属性组之间的间隔","。由于下个组位置数据在3个",[471,1638,473],{},"之后，我们把步长设置为",[471,1641,1642],{},"3 * sizeof(float)","。要注意的是由于我们知道这个数组是紧密排列的（在两个顶点属性之间没有空隙）我们也可以设置为0来让OpenGL决定具体步长是多少（只有当数值是紧密排列时才可用）",[33,1645,1646],{},"。一旦我们有更多的顶点属性，我们就必须更小心地定义每个顶点属性之间的间隔","，我们在后面会看到更多的例子（译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节）。",[17,1649,1650,1651,1654,1655,78,1658,1661],{},"最后一个参数的类型是",[471,1652,1653],{},"void*","，所以需要我们进行这个奇怪的强制类型转换。它",[33,1656,1657],{},"表示位置数据在缓冲中起始位置的偏移量(Offset)",[33,1659,1660],{},"由于位置数据在数组的开头，所以这里是0","。我们会在后面详细解释这个参数。",[57,1663,1664,1668],{},[10,1665,1666],{},[62,1667,64],{},[10,1669,1670,1673,1674,1676],{},[33,1671,1672],{},"每个顶点属性从一个VBO管理的内存中获得它的数据","，而具体是从哪个VBO（程序中可以有多个VBO）获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象，顶点属性",[471,1675,1513],{},"现在会链接到它的顶点数据。",[10,1678,1679,1680,1683,1684,1687,1688,1691],{},"​\t现在我们已经定义了OpenGL该如何解释顶点数据，",[33,1681,1682],{},"我们现在应该使用glEnableVertexAttribArray","，以顶点属性位置值作为参数，启用顶点属性；顶点属性默认是禁用的。自此，所有东西都已经设置好了：我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中，",[33,1685,1686],{},"建立了一个顶点和一个片段着色器","，并告诉了OpenGL如何",[33,1689,1690],{},"把顶点数据链接到顶点着色器的顶点属性上","。在OpenGL中绘制一个物体，代码会像是这样：",[476,1693,1695],{"className":478,"code":1694,"language":480,"meta":481,"style":481},"\u002F\u002F 0. 复制顶点数组到缓冲中供OpenGL使用\nglBindBuffer(GL_ARRAY_BUFFER, VBO);\nglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\u002F\u002F 1. 设置顶点属性指针\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\nglEnableVertexAttribArray(0);\n\u002F\u002F 2. 当我们渲染一个物体时要使用着色器程序\nglUseProgram(shaderProgram);\n\u002F\u002F 3. 绘制物体\nsomeOpenGLFunctionThatDrawsOurTriangle();\n",[471,1696,1697,1702,1706,1710,1715,1719,1723,1728,1733,1739],{"__ignoreMap":481},[62,1698,1699],{"class":486,"line":487},[62,1700,1701],{},"\u002F\u002F 0. 复制顶点数组到缓冲中供OpenGL使用\n",[62,1703,1704],{"class":486,"line":493},[62,1705,636],{},[62,1707,1708],{"class":486,"line":499},[62,1709,649],{},[62,1711,1712],{"class":486,"line":505},[62,1713,1714],{},"\u002F\u002F 1. 设置顶点属性指针\n",[62,1716,1717],{"class":486,"line":511},[62,1718,1481],{},[62,1720,1721],{"class":486,"line":849},[62,1722,1486],{},[62,1724,1725],{"class":486,"line":855},[62,1726,1727],{},"\u002F\u002F 2. 当我们渲染一个物体时要使用着色器程序\n",[62,1729,1731],{"class":486,"line":1730},8,[62,1732,1395],{},[62,1734,1736],{"class":486,"line":1735},9,[62,1737,1738],{},"\u002F\u002F 3. 绘制物体\n",[62,1740,1742],{"class":486,"line":1741},10,[62,1743,1744],{},"someOpenGLFunctionThatDrawsOurTriangle();\n",[10,1746,1747],{},"​\t每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多，但是如果有超过5个顶点属性，上百个不同物体呢（这其实并不罕见）。绑定正确的缓冲对象，为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中，并且可以通过绑定这个对象来恢复状态呢？",[102,1749,1750],{"id":1750},"顶点数组对象",[10,1752,81,1753,1756,1757,1760,1761,1764],{},[33,1754,1755],{},"顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定","，任何随后的顶点属性调用都会",[33,1758,1759],{},"储存在这个VAO中","。这样的好处就是，当配置顶点属性指针时，你只需要将那些调用执行一次，",[33,1762,1763],{},"之后再绘制物体的时候只需要绑定相应的VAO就行了","。这使在不同顶点数据和属性配置之间切换变得非常简单，只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中",[57,1766,1767,1771],{},[10,1768,1769],{},[62,1770,64],{},[10,1772,1773,1774,1777],{},"OpenGL的核心模式",[33,1775,1776],{},"要求","我们使用VAO，所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败，OpenGL会拒绝绘制任何东西。",[10,1779,1780],{},"一个顶点数组对象会储存以下这些内容：",[14,1782,1783,1786,1789],{},[17,1784,1785],{},"glEnableVertexAttribArray和glDisableVertexAttribArray的调用。",[17,1787,1788],{},"通过glVertexAttribPointer设置的顶点属性配置。",[17,1790,1791],{},"通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。",[10,1793,1794],{},[283,1795],{"alt":283,"src":1796},".\u002Fassets\u002Fvertex_array_objects.png",[10,1798,1799],{},"创建一个VAO和创建一个VBO很类似：",[476,1801,1803],{"className":478,"code":1802,"language":480,"meta":481,"style":481},"unsigned int VAO;\nglGenVertexArrays(1, &VAO);\n",[471,1804,1805,1810],{"__ignoreMap":481},[62,1806,1807],{"class":486,"line":487},[62,1808,1809],{},"unsigned int VAO;\n",[62,1811,1812],{"class":486,"line":493},[62,1813,1814],{},"glGenVertexArrays(1, &VAO);\n",[10,1816,1817,1818,1821,1822,1825,1826,1829,1830,1833],{},"​\t要想使用VAO，要做的只是",[33,1819,1820],{},"使用glBindVertexArray绑定VAO","。从绑定之后起，",[33,1823,1824],{},"我们应该绑定和配置对应的VBO和属性指针","，之后",[33,1827,1828],{},"解绑VAO供之后使用","。当我们打算绘制一个物体的时候，我们只要",[33,1831,1832],{},"在绘制物体前简单地把VAO绑定到希望使用的设定上就行了","。这段代码应该看起来像这样：",[476,1835,1837],{"className":478,"code":1836,"language":480,"meta":481,"style":481},"\u002F\u002F ..:: 初始化代码（只运行一次 (除非你的物体频繁改变)） :: ..\n\u002F\u002F 1. 绑定VAO\nglBindVertexArray(VAO);\n\u002F\u002F 2. 把顶点数组复制到缓冲中供OpenGL使用\nglBindBuffer(GL_ARRAY_BUFFER, VBO);\nglBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\u002F\u002F 3. 设置顶点属性指针\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\nglEnableVertexAttribArray(0);\n\n[...]\n\n\u002F\u002F ..:: 绘制代码（渲染循环中） :: ..\n\u002F\u002F 4. 绘制物体\nglUseProgram(shaderProgram);\nglBindVertexArray(VAO);\nsomeOpenGLFunctionThatDrawsOurTriangle();\n",[471,1838,1839,1844,1849,1854,1859,1863,1867,1872,1876,1880,1884,1890,1895,1901,1907,1912,1917],{"__ignoreMap":481},[62,1840,1841],{"class":486,"line":487},[62,1842,1843],{},"\u002F\u002F ..:: 初始化代码（只运行一次 (除非你的物体频繁改变)） :: ..\n",[62,1845,1846],{"class":486,"line":493},[62,1847,1848],{},"\u002F\u002F 1. 绑定VAO\n",[62,1850,1851],{"class":486,"line":499},[62,1852,1853],{},"glBindVertexArray(VAO);\n",[62,1855,1856],{"class":486,"line":505},[62,1857,1858],{},"\u002F\u002F 2. 把顶点数组复制到缓冲中供OpenGL使用\n",[62,1860,1861],{"class":486,"line":511},[62,1862,636],{},[62,1864,1865],{"class":486,"line":849},[62,1866,649],{},[62,1868,1869],{"class":486,"line":855},[62,1870,1871],{},"\u002F\u002F 3. 设置顶点属性指针\n",[62,1873,1874],{"class":486,"line":1730},[62,1875,1481],{},[62,1877,1878],{"class":486,"line":1735},[62,1879,1486],{},[62,1881,1882],{"class":486,"line":1741},[62,1883,836],{"emptyLinePlaceholder":835},[62,1885,1887],{"class":486,"line":1886},11,[62,1888,1889],{},"[...]\n",[62,1891,1893],{"class":486,"line":1892},12,[62,1894,836],{"emptyLinePlaceholder":835},[62,1896,1898],{"class":486,"line":1897},13,[62,1899,1900],{},"\u002F\u002F ..:: 绘制代码（渲染循环中） :: ..\n",[62,1902,1904],{"class":486,"line":1903},14,[62,1905,1906],{},"\u002F\u002F 4. 绘制物体\n",[62,1908,1910],{"class":486,"line":1909},15,[62,1911,1395],{},[62,1913,1915],{"class":486,"line":1914},16,[62,1916,1853],{},[62,1918,1920],{"class":486,"line":1919},17,[62,1921,1744],{},[10,1923,1924,1925,1928,1929,1932,1933,78],{},"​\t就这么多了！前面做的一切都是等待这一刻，",[33,1926,1927],{},"一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象","。一般",[33,1930,1931],{},"当你打算绘制多个物体时，你首先要生成\u002F配置所有的VAO（和必须的VBO及属性指针)","，然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO，",[33,1934,1935],{},"绑定它，绘制完物体后，再解绑VAO",[102,1937,1938],{"id":1938},"我们一直期待的三角形",[10,1940,1941,1942,51,1945,1948],{},"​\t要想绘制我们想要的物体，",[33,1943,1944],{},"OpenGL给我们提供了glDrawArrays函数，它使用当前激活的着色器",[33,1946,1947],{},"之前定义的顶点属性配置","，和**VBO的顶点数据（通过VAO间接绑定）**来绘制图元。",[476,1950,1952],{"className":478,"code":1951,"language":480,"meta":481,"style":481},"glUseProgram(shaderProgram);\nglBindVertexArray(VAO);\nglDrawArrays(GL_TRIANGLES, 0, 3);\n",[471,1953,1954,1958,1962],{"__ignoreMap":481},[62,1955,1956],{"class":486,"line":487},[62,1957,1395],{},[62,1959,1960],{"class":486,"line":493},[62,1961,1853],{},[62,1963,1964],{"class":486,"line":499},[62,1965,1966],{},"glDrawArrays(GL_TRIANGLES, 0, 3);\n",[10,1968,81,1969,1972,1973,1975,1976,1978],{},[33,1970,1971],{},"glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型","。由于我们在一开始时说过，我们希望绘制的是一个三角形，这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引，我们这里填",[471,1974,1513],{},"。最后一个参数指定我们打算绘制多少个顶点，这里是",[471,1977,1537],{},"（我们只从我们的数据中渲染一个三角形，它只有3个顶点长）。",[10,1980,1981],{},"现在尝试编译代码，如果弹出了任何错误，回头检查你的代码。如果你编译通过了，你应该看到下面的结果：",[10,1983,1984],{},[283,1985],{"alt":283,"src":1986},".\u002Fassets\u002Fhellotriangle.png",[476,1988,1990],{"className":478,"code":1989,"language":480,"meta":481,"style":481},"#include \u003Cglad\u002Fglad.h>\n#include \u003CGLFW\u002Fglfw3.h>\n#include \u003Ciostream>\n\n\u002F\u002F 窗口大小改变时的回调函数\nvoid framebuffer_size_callback(GLFWwindow* window, int width, int height) {\n    glViewport(0, 0, width, height);\n}\n\nint main() {\n    \u002F\u002F 初始化GLFW\n    glfwInit();\n    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);\n    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);\n\n    \u002F\u002F 创建窗口\n    GLFWwindow* window = glfwCreateWindow(800, 600, \"OpenGL Window\", NULL, NULL);\n    if (window == NULL) {\n        std::cout \u003C\u003C \"Failed to create GLFW window\" \u003C\u003C std::endl;\n        glfwTerminate();\n        return -1;\n    }\n    \n    glfwMakeContextCurrent(window);\n    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);\n\n    \u002F\u002F 初始化GLAD\n    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {\n        std::cout \u003C\u003C \"Failed to initialize GLAD\" \u003C\u003C std::endl;\n        return -1;\n    }\n\n    \u002F\u002F 顶点数据\n    float vertices[] = {\n        -0.5f, -0.5f, 0.0f,\n         0.5f, -0.5f, 0.0f,\n         0.0f,  0.5f, 0.0f\n    };\n\n    \u002F\u002F 创建VAO\n    unsigned int VAO;\n    glGenVertexArrays(1, &VAO);\n    glBindVertexArray(VAO);\n\n    \u002F\u002F 创建VBO\n    unsigned int VBO;\n    glGenBuffers(1, &VBO);\n    glBindBuffer(GL_ARRAY_BUFFER, VBO);\n    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n\n    \u002F\u002F 设置顶点属性\n    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\n    glEnableVertexAttribArray(0);\n\n    \u002F\u002F 顶点着色器\n    const char* vertexShaderSource = \n        \"#version 330 core\\n\"\n        \"layout (location = 0) in vec3 aPos;\\n\"\n        \"void main()\\n\"\n        \"{\\n\"\n        \"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n\"\n        \"}\\0\";\n\n    unsigned int vertexShader;\n    vertexShader = glCreateShader(GL_VERTEX_SHADER);\n    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\n    glCompileShader(vertexShader);\n\n    \u002F\u002F 片段着色器\n    const char* fragmentShaderSource = \n        \"#version 330 core\\n\"\n        \"out vec4 FragColor;\\n\"\n        \"void main()\\n\"\n        \"{\\n\"\n        \"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\\n\"\n        \"}\\0\";\n    \n    unsigned int fragmentShader;\n    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\n    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\n    glCompileShader(fragmentShader);\n\n    \u002F\u002F 创建着色器程序\n    unsigned int shaderProgram;\n    shaderProgram = glCreateProgram();\n    glAttachShader(shaderProgram, vertexShader);\n    glAttachShader(shaderProgram, fragmentShader);\n    glLinkProgram(shaderProgram);\n\n    \u002F\u002F 删除着色器对象\n    glDeleteShader(vertexShader);\n    glDeleteShader(fragmentShader);\n\n    \u002F\u002F 渲染循环\n    while (!glfwWindowShouldClose(window)) {\n        \u002F\u002F 处理输入\n        glfwPollEvents();\n        \n        \u002F\u002F 渲染指令\n        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);\n        glClear(GL_COLOR_BUFFER_BIT);\n        \n        \u002F\u002F 使用着色器程序\n        glUseProgram(shaderProgram);\n        glBindVertexArray(VAO);\n        \n        \u002F\u002F 绘制三角形\n        glDrawArrays(GL_TRIANGLES, 0, 3);\n        \n        \u002F\u002F 交换缓冲区和检查IO事件\n        glfwSwapBuffers(window);\n    }\n\n    \u002F\u002F 清理资源\n    glDeleteVertexArrays(1, &VAO);\n    glDeleteBuffers(1, &VBO);\n    glDeleteProgram(shaderProgram);\n    glfwTerminate();\n    return 0;\n}\n",[471,1991,1992,1997,2002,2007,2011,2016,2021,2026,2030,2034,2039,2044,2049,2054,2059,2064,2068,2073,2079,2085,2091,2097,2103,2109,2115,2121,2127,2132,2138,2144,2150,2155,2160,2165,2171,2177,2183,2189,2195,2201,2206,2212,2218,2224,2230,2235,2241,2247,2253,2259,2265,2270,2276,2282,2288,2293,2299,2305,2311,2317,2323,2329,2335,2341,2346,2352,2358,2364,2370,2375,2381,2387,2392,2398,2403,2408,2414,2419,2424,2430,2436,2442,2448,2453,2459,2465,2471,2477,2483,2489,2494,2500,2506,2512,2517,2523,2529,2535,2541,2547,2553,2559,2565,2570,2576,2582,2588,2593,2599,2605,2610,2616,2622,2627,2632,2638,2644,2650,2656,2662,2668],{"__ignoreMap":481},[62,1993,1994],{"class":486,"line":487},[62,1995,1996],{},"#include \u003Cglad\u002Fglad.h>\n",[62,1998,1999],{"class":486,"line":493},[62,2000,2001],{},"#include \u003CGLFW\u002Fglfw3.h>\n",[62,2003,2004],{"class":486,"line":499},[62,2005,2006],{},"#include \u003Ciostream>\n",[62,2008,2009],{"class":486,"line":505},[62,2010,836],{"emptyLinePlaceholder":835},[62,2012,2013],{"class":486,"line":511},[62,2014,2015],{},"\u002F\u002F 窗口大小改变时的回调函数\n",[62,2017,2018],{"class":486,"line":849},[62,2019,2020],{},"void framebuffer_size_callback(GLFWwindow* window, int width, int height) {\n",[62,2022,2023],{"class":486,"line":855},[62,2024,2025],{},"    glViewport(0, 0, width, height);\n",[62,2027,2028],{"class":486,"line":1730},[62,2029,858],{},[62,2031,2032],{"class":486,"line":1735},[62,2033,836],{"emptyLinePlaceholder":835},[62,2035,2036],{"class":486,"line":1741},[62,2037,2038],{},"int main() {\n",[62,2040,2041],{"class":486,"line":1886},[62,2042,2043],{},"    \u002F\u002F 初始化GLFW\n",[62,2045,2046],{"class":486,"line":1892},[62,2047,2048],{},"    glfwInit();\n",[62,2050,2051],{"class":486,"line":1897},[62,2052,2053],{},"    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);\n",[62,2055,2056],{"class":486,"line":1903},[62,2057,2058],{},"    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);\n",[62,2060,2061],{"class":486,"line":1909},[62,2062,2063],{},"    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);\n",[62,2065,2066],{"class":486,"line":1914},[62,2067,836],{"emptyLinePlaceholder":835},[62,2069,2070],{"class":486,"line":1919},[62,2071,2072],{},"    \u002F\u002F 创建窗口\n",[62,2074,2076],{"class":486,"line":2075},18,[62,2077,2078],{},"    GLFWwindow* window = glfwCreateWindow(800, 600, \"OpenGL Window\", NULL, NULL);\n",[62,2080,2082],{"class":486,"line":2081},19,[62,2083,2084],{},"    if (window == NULL) {\n",[62,2086,2088],{"class":486,"line":2087},20,[62,2089,2090],{},"        std::cout \u003C\u003C \"Failed to create GLFW window\" \u003C\u003C std::endl;\n",[62,2092,2094],{"class":486,"line":2093},21,[62,2095,2096],{},"        glfwTerminate();\n",[62,2098,2100],{"class":486,"line":2099},22,[62,2101,2102],{},"        return -1;\n",[62,2104,2106],{"class":486,"line":2105},23,[62,2107,2108],{},"    }\n",[62,2110,2112],{"class":486,"line":2111},24,[62,2113,2114],{},"    \n",[62,2116,2118],{"class":486,"line":2117},25,[62,2119,2120],{},"    glfwMakeContextCurrent(window);\n",[62,2122,2124],{"class":486,"line":2123},26,[62,2125,2126],{},"    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);\n",[62,2128,2130],{"class":486,"line":2129},27,[62,2131,836],{"emptyLinePlaceholder":835},[62,2133,2135],{"class":486,"line":2134},28,[62,2136,2137],{},"    \u002F\u002F 初始化GLAD\n",[62,2139,2141],{"class":486,"line":2140},29,[62,2142,2143],{},"    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {\n",[62,2145,2147],{"class":486,"line":2146},30,[62,2148,2149],{},"        std::cout \u003C\u003C \"Failed to initialize GLAD\" \u003C\u003C std::endl;\n",[62,2151,2153],{"class":486,"line":2152},31,[62,2154,2102],{},[62,2156,2158],{"class":486,"line":2157},32,[62,2159,2108],{},[62,2161,2163],{"class":486,"line":2162},33,[62,2164,836],{"emptyLinePlaceholder":835},[62,2166,2168],{"class":486,"line":2167},34,[62,2169,2170],{},"    \u002F\u002F 顶点数据\n",[62,2172,2174],{"class":486,"line":2173},35,[62,2175,2176],{},"    float vertices[] = {\n",[62,2178,2180],{"class":486,"line":2179},36,[62,2181,2182],{},"        -0.5f, -0.5f, 0.0f,\n",[62,2184,2186],{"class":486,"line":2185},37,[62,2187,2188],{},"         0.5f, -0.5f, 0.0f,\n",[62,2190,2192],{"class":486,"line":2191},38,[62,2193,2194],{},"         0.0f,  0.5f, 0.0f\n",[62,2196,2198],{"class":486,"line":2197},39,[62,2199,2200],{},"    };\n",[62,2202,2204],{"class":486,"line":2203},40,[62,2205,836],{"emptyLinePlaceholder":835},[62,2207,2209],{"class":486,"line":2208},41,[62,2210,2211],{},"    \u002F\u002F 创建VAO\n",[62,2213,2215],{"class":486,"line":2214},42,[62,2216,2217],{},"    unsigned int VAO;\n",[62,2219,2221],{"class":486,"line":2220},43,[62,2222,2223],{},"    glGenVertexArrays(1, &VAO);\n",[62,2225,2227],{"class":486,"line":2226},44,[62,2228,2229],{},"    glBindVertexArray(VAO);\n",[62,2231,2233],{"class":486,"line":2232},45,[62,2234,836],{"emptyLinePlaceholder":835},[62,2236,2238],{"class":486,"line":2237},46,[62,2239,2240],{},"    \u002F\u002F 创建VBO\n",[62,2242,2244],{"class":486,"line":2243},47,[62,2245,2246],{},"    unsigned int VBO;\n",[62,2248,2250],{"class":486,"line":2249},48,[62,2251,2252],{},"    glGenBuffers(1, &VBO);\n",[62,2254,2256],{"class":486,"line":2255},49,[62,2257,2258],{},"    glBindBuffer(GL_ARRAY_BUFFER, VBO);\n",[62,2260,2262],{"class":486,"line":2261},50,[62,2263,2264],{},"    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);\n",[62,2266,2268],{"class":486,"line":2267},51,[62,2269,836],{"emptyLinePlaceholder":835},[62,2271,2273],{"class":486,"line":2272},52,[62,2274,2275],{},"    \u002F\u002F 设置顶点属性\n",[62,2277,2279],{"class":486,"line":2278},53,[62,2280,2281],{},"    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);\n",[62,2283,2285],{"class":486,"line":2284},54,[62,2286,2287],{},"    glEnableVertexAttribArray(0);\n",[62,2289,2291],{"class":486,"line":2290},55,[62,2292,836],{"emptyLinePlaceholder":835},[62,2294,2296],{"class":486,"line":2295},56,[62,2297,2298],{},"    \u002F\u002F 顶点着色器\n",[62,2300,2302],{"class":486,"line":2301},57,[62,2303,2304],{},"    const char* vertexShaderSource = \n",[62,2306,2308],{"class":486,"line":2307},58,[62,2309,2310],{},"        \"#version 330 core\\n\"\n",[62,2312,2314],{"class":486,"line":2313},59,[62,2315,2316],{},"        \"layout (location = 0) in vec3 aPos;\\n\"\n",[62,2318,2320],{"class":486,"line":2319},60,[62,2321,2322],{},"        \"void main()\\n\"\n",[62,2324,2326],{"class":486,"line":2325},61,[62,2327,2328],{},"        \"{\\n\"\n",[62,2330,2332],{"class":486,"line":2331},62,[62,2333,2334],{},"        \"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\\n\"\n",[62,2336,2338],{"class":486,"line":2337},63,[62,2339,2340],{},"        \"}\\0\";\n",[62,2342,2344],{"class":486,"line":2343},64,[62,2345,836],{"emptyLinePlaceholder":835},[62,2347,2349],{"class":486,"line":2348},65,[62,2350,2351],{},"    unsigned int vertexShader;\n",[62,2353,2355],{"class":486,"line":2354},66,[62,2356,2357],{},"    vertexShader = glCreateShader(GL_VERTEX_SHADER);\n",[62,2359,2361],{"class":486,"line":2360},67,[62,2362,2363],{},"    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);\n",[62,2365,2367],{"class":486,"line":2366},68,[62,2368,2369],{},"    glCompileShader(vertexShader);\n",[62,2371,2373],{"class":486,"line":2372},69,[62,2374,836],{"emptyLinePlaceholder":835},[62,2376,2378],{"class":486,"line":2377},70,[62,2379,2380],{},"    \u002F\u002F 片段着色器\n",[62,2382,2384],{"class":486,"line":2383},71,[62,2385,2386],{},"    const char* fragmentShaderSource = \n",[62,2388,2390],{"class":486,"line":2389},72,[62,2391,2310],{},[62,2393,2395],{"class":486,"line":2394},73,[62,2396,2397],{},"        \"out vec4 FragColor;\\n\"\n",[62,2399,2401],{"class":486,"line":2400},74,[62,2402,2322],{},[62,2404,2406],{"class":486,"line":2405},75,[62,2407,2328],{},[62,2409,2411],{"class":486,"line":2410},76,[62,2412,2413],{},"        \"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\\n\"\n",[62,2415,2417],{"class":486,"line":2416},77,[62,2418,2340],{},[62,2420,2422],{"class":486,"line":2421},78,[62,2423,2114],{},[62,2425,2427],{"class":486,"line":2426},79,[62,2428,2429],{},"    unsigned int fragmentShader;\n",[62,2431,2433],{"class":486,"line":2432},80,[62,2434,2435],{},"    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);\n",[62,2437,2439],{"class":486,"line":2438},81,[62,2440,2441],{},"    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);\n",[62,2443,2445],{"class":486,"line":2444},82,[62,2446,2447],{},"    glCompileShader(fragmentShader);\n",[62,2449,2451],{"class":486,"line":2450},83,[62,2452,836],{"emptyLinePlaceholder":835},[62,2454,2456],{"class":486,"line":2455},84,[62,2457,2458],{},"    \u002F\u002F 创建着色器程序\n",[62,2460,2462],{"class":486,"line":2461},85,[62,2463,2464],{},"    unsigned int shaderProgram;\n",[62,2466,2468],{"class":486,"line":2467},86,[62,2469,2470],{},"    shaderProgram = glCreateProgram();\n",[62,2472,2474],{"class":486,"line":2473},87,[62,2475,2476],{},"    glAttachShader(shaderProgram, vertexShader);\n",[62,2478,2480],{"class":486,"line":2479},88,[62,2481,2482],{},"    glAttachShader(shaderProgram, fragmentShader);\n",[62,2484,2486],{"class":486,"line":2485},89,[62,2487,2488],{},"    glLinkProgram(shaderProgram);\n",[62,2490,2492],{"class":486,"line":2491},90,[62,2493,836],{"emptyLinePlaceholder":835},[62,2495,2497],{"class":486,"line":2496},91,[62,2498,2499],{},"    \u002F\u002F 删除着色器对象\n",[62,2501,2503],{"class":486,"line":2502},92,[62,2504,2505],{},"    glDeleteShader(vertexShader);\n",[62,2507,2509],{"class":486,"line":2508},93,[62,2510,2511],{},"    glDeleteShader(fragmentShader);\n",[62,2513,2515],{"class":486,"line":2514},94,[62,2516,836],{"emptyLinePlaceholder":835},[62,2518,2520],{"class":486,"line":2519},95,[62,2521,2522],{},"    \u002F\u002F 渲染循环\n",[62,2524,2526],{"class":486,"line":2525},96,[62,2527,2528],{},"    while (!glfwWindowShouldClose(window)) {\n",[62,2530,2532],{"class":486,"line":2531},97,[62,2533,2534],{},"        \u002F\u002F 处理输入\n",[62,2536,2538],{"class":486,"line":2537},98,[62,2539,2540],{},"        glfwPollEvents();\n",[62,2542,2544],{"class":486,"line":2543},99,[62,2545,2546],{},"        \n",[62,2548,2550],{"class":486,"line":2549},100,[62,2551,2552],{},"        \u002F\u002F 渲染指令\n",[62,2554,2556],{"class":486,"line":2555},101,[62,2557,2558],{},"        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);\n",[62,2560,2562],{"class":486,"line":2561},102,[62,2563,2564],{},"        glClear(GL_COLOR_BUFFER_BIT);\n",[62,2566,2568],{"class":486,"line":2567},103,[62,2569,2546],{},[62,2571,2573],{"class":486,"line":2572},104,[62,2574,2575],{},"        \u002F\u002F 使用着色器程序\n",[62,2577,2579],{"class":486,"line":2578},105,[62,2580,2581],{},"        glUseProgram(shaderProgram);\n",[62,2583,2585],{"class":486,"line":2584},106,[62,2586,2587],{},"        glBindVertexArray(VAO);\n",[62,2589,2591],{"class":486,"line":2590},107,[62,2592,2546],{},[62,2594,2596],{"class":486,"line":2595},108,[62,2597,2598],{},"        \u002F\u002F 绘制三角形\n",[62,2600,2602],{"class":486,"line":2601},109,[62,2603,2604],{},"        glDrawArrays(GL_TRIANGLES, 0, 3);\n",[62,2606,2608],{"class":486,"line":2607},110,[62,2609,2546],{},[62,2611,2613],{"class":486,"line":2612},111,[62,2614,2615],{},"        \u002F\u002F 交换缓冲区和检查IO事件\n",[62,2617,2619],{"class":486,"line":2618},112,[62,2620,2621],{},"        glfwSwapBuffers(window);\n",[62,2623,2625],{"class":486,"line":2624},113,[62,2626,2108],{},[62,2628,2630],{"class":486,"line":2629},114,[62,2631,836],{"emptyLinePlaceholder":835},[62,2633,2635],{"class":486,"line":2634},115,[62,2636,2637],{},"    \u002F\u002F 清理资源\n",[62,2639,2641],{"class":486,"line":2640},116,[62,2642,2643],{},"    glDeleteVertexArrays(1, &VAO);\n",[62,2645,2647],{"class":486,"line":2646},117,[62,2648,2649],{},"    glDeleteBuffers(1, &VBO);\n",[62,2651,2653],{"class":486,"line":2652},118,[62,2654,2655],{},"    glDeleteProgram(shaderProgram);\n",[62,2657,2659],{"class":486,"line":2658},119,[62,2660,2661],{},"    glfwTerminate();\n",[62,2663,2665],{"class":486,"line":2664},120,[62,2666,2667],{},"    return 0;\n",[62,2669,2671],{"class":486,"line":2670},121,[62,2672,858],{},[2674,2675,2676],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":481,"searchDepth":493,"depth":493,"links":2678},[2679,2683,2684,2685,2686,2687,2690],{"id":99,"depth":493,"text":100,"children":2680},[2681,2682],{"id":104,"depth":499,"text":105},{"id":137,"depth":499,"text":138},{"id":222,"depth":493,"text":223},{"id":451,"depth":493,"text":451},{"id":805,"depth":493,"text":805},{"id":996,"depth":493,"text":996},{"id":1171,"depth":493,"text":1171,"children":2688},[2689],{"id":1280,"depth":499,"text":1280},{"id":1432,"depth":493,"text":1432,"children":2691},[2692,2693],{"id":1750,"depth":499,"text":1750},{"id":1938,"depth":499,"text":1938},"md",{},"\u002Fcpp\u002Fopengel\u002F三角形绘制",{"description":12},"cpp\u002Fopengel\u002F三角形绘制","YrKzUyjDH88Bs4zyjl3b8szMZlzUHId94Ll4fsQkyrY",1791042462991]