OpenGL和WebGL入门(openGL入门)

小编:饿狼 更新时间:2022-08-21

OpenGL是业界使用最广泛的2D和3D图形API,将成千上万的应用程序带到各种计算机平台上。它与窗口系统和操作系统无关,并且网络透明。 OpenGL使PC、工作站和超级计算类型硬件的软件开发人员能够在CAD、内容创建、能源、娱乐、游戏开发、制造、医疗和虚拟现实等市场中创建高性能,视觉上引人注目的图形软件应用程序。 OpenGL公开了最新图形硬件的所有功能。经常做UI的开发者有可能一眼就看出OpenGL的UI界面和其它技术制作界面的区别。OpenGL第一个版本就很成熟,后续不断升级,是一个超级平台,欢迎大家来学习OpenGL开发。

OpenGL 4.6概览

OpenGL 4.6和OpenGL阴影语言4.60规范于2017年7月31日发布。

OpenGL 4.6的新功能包括:

GL_ARB_gl_spirv和GL_ARB_spirv_extensions标准化对OpenGL的SPIR-V支持
GL_ARB_indirect_parameters和GL_ARB_shader_draw_parameters用于减少与渲染几何批量相关的CPU开销
GL_ARB_pipeline_statistics_query和GL_ARB_transform_feedback_overflow_query标准化OpenGL对Direct3D中可用功能的支持
GL_ARB_texture_filter_anisotropic(基于GL_EXT_texture_filter_anisotropic)将先前IP限制的功能引入OpenGL,以改善纹理场景的视觉质量
GL_ARB_polygon_offset_clamp(基于GL_EXT_polygon_offset_clamp)可抑制与渲染阴影相关的常见视觉伪影,即“漏光”
GL_ARB_shader_atomic_counter_ops和GL_ARB_shader_group_vote添加了所有桌面供应商支持的着色器内在函数,以改善功能和性能
GL_KHR_no_error通过允许应用程序指示其期望无错误操作来减少驱动程序开销,因此无需生成错误
OpenGL 4.6的新扩展包括:

GL_KHR_parallel_shader_compileWGL_ARB_create_context_no_errorGLX_ARB_create_context_no_errorGL_EXT_memory_objectGL_EXT_memory_object_fdGL_EXT_memory_object_win32GL_EXT_semaphoreGL_EXT_semaphore_fdGL_EXT_semaphore_win32GL_EXT_win32_keyed_mutex

WebGL入门

Web前端开发比较热门,这里专门介绍WebGL开发。其它介绍请关注上面OpenGL开源社区里的链接

Web应用程序很有吸引力,因为它们是共享您的工作的绝佳方式。用户所需要做的只是打开一个Web链接。 WebGL是浏览器实现的界面,可让Web应用程序访问功能强大的硬件加速渲染。无需插件或安装。 WebGL基于OpenGL的移动版本; “ OpenGL ES”不需要最新的图形硬件,具有很高的便携性,并且可以在较旧的台式机和大多数移动设备上运行。而且,甚至更好的是,您无需为每个操作系统都进行特殊的构建。您可能会发现WebGL创建程序的速度要比台式机和移动设备快几倍。
我将在本页顶部解释如何制作旋转字符,并介绍基本的启动,JavaScript的使用,加载纹理,着色器,网格和文件加载以及矩阵处理。为了避免多余的教程内容,我将假设您至少已经完成了现代OpenGL编程的基础知识-这意味着您对着色器和顶点缓冲区有所了解。我将假定读者熟悉基本的HTML,但不一定熟悉JavaScript或更新的Web界面。我们将不会使用任何框架或高级接口,因为已经有足够的资源来使用这些框架或高级接口。

技术概述

编写WebGL软件时,您会发现自己在几种语言之间切换。

Language 功能

HTML5 编写带矩形画布的页面,画布内进行渲染

JavaScript 调用GL函数,用AJAX加载资源,处理用户输入,编写主逻辑

GLSL 编写shaders来定义渲染的风格

我们将只使用5中的一项或多项新功能来编写一些基本的HTML。JavaScript具有一个文件加载接口,通常称为“ AJAX”(异步JavaScript和XML),因为它旨在将XML文件序列化为JavaScript。对象,但我们将其用作通用文件加载器,该加载器返回包含文件内容的字符串。着色器语言有一个或两个非常小的区别,与OpenGL ES相同,具有基于OpenGL ES 2.0规范的WebGL 1.0和基于ES 3.0的WebGL 2.0。

Basic HTML Skeleton 基本HTML骨架

我们可以从制作一个非常简单的HTML网页开始。

我们首先制作一个简单的HTML网页,上面有一个画布区域(以阴影显示)。我们将能够使用任何其他Web界面元素与我们的代码进行交互。
WebGL软件的基本概念是我们编写一个普通的网页,并使用新的HTML5画布标签定义渲染将绘制到的页面区域(空白矩形)。您可以使用HTML的任何形式,文本区域和其他元素来与您的可视化进行交互-可以说我们内置了用户界面库。
我开始是这样的:

This text will appear if the browser doesn't support HTML5.
A model from a recent Ludum Dare game jam.You can view the source code of the page to see how I display this.

如果您在浏览器中打开它,您将看不到任何东西-只是默认300x150像素画布所在的空间。我们将在不久后为其附加GL上下文。为此,我们添加一些JavaScript,它将与我们的Web文档对话。
在我们的HTML中,画布是一个简单的标签,带有DOM ID作为代码钩。我们还可以提供宽度和高度属性来指定页面上画布的实际大小。
我们利用DOM(文档对象模型)从我们的JavaScript代码访问网页元素。这就像给每个元素的HTML标签提供id =“ my_thingy”属性一样简单。该浏览器还具有BOM(浏览器对象模型),该BOM提供了内置功能来处理通过鼠标,游戏手柄或键盘进行的用户交互。

JavaScript启动代码

JavaScript取代了C作为图形库的主要接口语言。如果您以前从未使用过JavaScript,那么您应该知道除了名称之外,它与Java语言无关-从人们仍然认为Java是一个好主意的时代起,这就是一种营销计划! JavaScript是一种客户端脚本语言,这意味着客户端的Web浏览器会下载您的整个源代码,然后在自己的CPU上的浏览器中运行它。这意味着您可以执行功能更强大的交互式调试,并且不需要重新编译,但是比C慢一点。我们能在html内任何地方写script,不过我喜欢写在最后,需要加如下:

我在这里介绍一些新概念。第一条指令与printf()的JavaScript等效。如果转到浏览器的开发人员菜单,则可以打开JavaScript控制台,并且应该看到该消息。每次刷新或重新加载页面时都将其打开是很好的,因为它会为您提供很好的错误信息,您可能不会注意到这些错误信息,因为页面可能会继续看起来正确加载。

我使用DOM来获取我的canvas元素作为JavaScript对象。请注意,JavaScript不使用强类型-一切都是一个var对象,我可以告诉您这引起的问题远远超过其解决的问题。无论如何,我的第一个动作是从JavaScript修改画布的width和height属性,这应该更改其在页面上的大小。
我要求画布使用新的WebGL上下文进行设置,并将其作为名为gl的对象进行跟踪。该对象将成为我们所有WebGL功能的接口。
我的最终指令使用gl对象调用GL函数。它们几乎与OpenGL函数名称和常量相同,除了gl和GL_被删除了,我们通过新对象访问它们。如果您想使背景透明并且要显示页面背景,则可以在此处将aplha通道设置为0.0。如果刷新网页,您应该会看到画布变大并且变色了。
阅读参考,您会注意到WebGL不支持较新的OpenGL的“顶点数组对象”(VAO)。在没有VAO的OpenGL中,这实际上是非常繁琐的渲染,因为每次绘制时都必须设置顶点属性指针。 WebGL有一个VAO扩展。您可以在扩展注册表中看到它。我们可以在脚本块中查询它,这将返回一个新对象,该对象然后是我们与VAO扩展功能子集的接口:

var vao_ext = gl.getExtension ("OES_vertex_array_object");if (!vao_ext) { console.error ("ERROR: Your browser does not support WebGL VAO extension");}

如果用户的浏览器/系统不支持该功能,则可以使用浏览器控制台的错误日志记录机制进行报告。它还在控制台的输出中包括一个行号链接。

异步加载纹理

实际上,将纹理加载到WebGL中比使用常规OpenGL要容易得多,因为我们不需要图像加载库-HTML已经加载了图像。我们可以使用网页上显示的图像作为纹理,也可以在JavaScript中安静地加载图像。请注意,这是异步的,因此稍后会在您提供图像URL后的代码中实际创建纹理。

var texture = gl.createTexture();texture.is_loaded = false;var image = new Image();image.onload = function () { gl.bindTexture (gl.TEXTURE_2D, texture); gl.pixelStorei (gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D (gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.texParameteri (gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri (gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri (gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri (gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); texture.is_loaded = true; console.log ("texture loaded ");}image.src = "webgl_starter/heckler.png";

请注意,我做了一些非常不寻常的事情,并在纹理内设置了is_loaded属性。我们知道OpenGL纹理甚至都不是对象-它们当然没有属性!在JavaScript中,我们可以向现有对象动态添加新属性。 由于现代网络使用异步下载模型,因此我们无法确定何时实际下载资源。如果连接不良,主循环启动后一分钟内,加载功能可能甚至无法执行。同时,我们至少将拥有一个有效但为空的纹理,并带有一个要检查的标志。在足以复杂地异步加载文件的C程序中,我们将进行如下设置:

start_loading_shader();start_loading_texture();start_loading_mesh();while ( !everything_has_loaded() ) { sleep();}// end of loading function

我们不允许在JavaScript中使用跨线程的等待循环,因此,我们可以做的最好的事情就是在主循环中添加if语句检查,这样它就不会在所有必需的资源加载之前尝试绘制。例如:

if (texture.is_loaded) { gl.activeTexture (gl.TEXTURE0); gl.bindTexture (gl.TEXTURE_2D, texture); ... // draw with the texture}...// rest of drawing code

您会看到注入一些状态属性为什么有用的原因,以及我们在WebGL演示中看到的资源“弹出”效果从何而来。如果要避免这种奇怪的影响,您可以使用一个函数来检查是否在渲染之前加载了所有资源,而是渲染了一些“加载..”文本,但是从我的经验来看,这个大型商业项目有很多如果要下载大量资源数据,如果他们不得不在移动设备上或连接不良的情况下等待很长时间,则可能会带来非常糟糕的用户体验-您可能会发现用户宁愿选择“弹出”,也不愿等待很长时间。

从HTML脚本加载着色器

我通常希望将着色器存储在外部文件中,但这实际上在JavaScript中有点不方便,JavaScript也希望异步加载它们。对于多部分着色器,使用此机制实际上是一件烦人的事。您可以将两个着色器连接到一个文件中。您还可以将着色器存储在JavaScript字符串中。相反,大多数开发人员发现将着色器放置在自己的脚本块中更加方便,这样您就不必担心字符串格式或异步下载。我将顶点着色器放在其他脚本之上的自己的脚本块中:

我将属性的类型设置为适当的外观,以使浏览器不会认为应该使用JavaScript。我设置了id属性,以便以后获取。用于WebGL 1.0的GLSL使用较旧的OpenGL 2.1和OpenGL ES而不是较新的输入和输出的acoderibute和不同的关键字。同样,我在脚本块中有一个片段着色器:


我们用varying代替in,,以及内置的gl_FragColor代替了输出变量。 WebGL片段着色器需要在顶部具有确切的精度声明。
要将这些块的内容作为JavaScript字符串获取,请在加载网格后执行以下操作:

var el = document.getElementById ("heckler.vert"); var vs_str = el.innerHTML; el = document.getElementById ("heckler.frag"); var fs_str = el.innerHTML

我只是再次使用DOM来获取字符串。可以将着色器放在可见的文本区域中,然后实时编辑它们,而不是脚本块。 Web元素具有您可以定义的一系列.on ....()函数。当用户单击某些内容或更改了文本时,将触发回调函数-您的函数可以重新编译着色器。
之后,我将定期查看代码来编译着色器,并获取一些变量来保存制服的位置。请注意,我还分别将点,纹理坐标和法线的属性位置分别绑定到0、1和2。

var vs = gl.createShader (gl.VERTEX_SHADER); var fs = gl.createShader (gl.FRAGMENT_SHADER); gl.shaderSource (vs, vs_str); gl.shaderSource (fs, fs_str); gl.compileShader (vs); if (!gl.getShaderParameter (vs, gl.COMPILE_STATUS)) { console.error ("ERROR compiling vert shader. log: " + gl.getShaderInfoLog (vs)); } gl.compileShader (fs); if (!gl.getShaderParameter (fs, gl.COMPILE_STATUS)) { console.error ("ERROR compiling frag shader. log: " + gl.getShaderInfoLog (fs)); } var sp = gl.createProgram (); gl.attachShader (sp, vs); gl.attachShader (sp, fs); gl.bindAttribLocation (sp, 0, "vp"); gl.bindAttribLocation (sp, 1, "vt"); gl.bindAttribLocation (sp, 2, "vn"); gl.linkProgram (sp); if (!gl.getProgramParameter (sp, gl.LINK_STATUS)) { console.error ("ERROR linking program. log: " + gl.getProgramInfoLog (sp)); } gl.validateProgram (sp); if (!gl.getProgramParameter(sp, gl.VALIDATE_STATUS)) { console.error ("ERROR validating program. log: " + gl.getProgramInfoLog (sp)); } var heckler_PV_loc = gl.getUniformLocation (sp, "PV"); var heckler_M_loc = gl.getUniformLocation (sp, "M");

“ PV”是我的组合投影和视图矩阵,“ M”是我的模型矩阵。尽管类似于C语言中的OpenGL,但您可以看到诸如字符串函数之类的东西更容易处理。如果刷新浏览器并在控制台中查看,则如果该信息不起作用,则会出现错误消息(包括着色器和链接器日志)。如果要确保已加载着色器代码,则可以使用警报(vs_str);抛出带有顶点着色器字符串的警报框。

用AJAX加载网格文件

我有一个要渲染的网格文件-我为最近的游戏卡纸制作的Wavefront .obj文件。我想做的是将.obj文件读入一个字符串,然后使用JavaScript字符串解析函数(非常好)将其分解为点,纹理坐标和法线的列表。我们可以为此使用AJAX,其工作方式如下:

var xmlhttp = new XMLHttpRequest(); xmlhttp.open ("GET", "OUR_URL_STRING_HERE", true); xmlhttp.onload = function (e) { var str = xmlhttp.responseText; var lines = str.split ('\n'); for (var i = 0; i < lines.length; i++) { //...parsing code goes here... } } xmlhttp.send ();

我们获得一个到新XMLHTTPRequest(AJAX的真实名称)的接口。我们告诉它它将使用HTTP“ GET”请求从URL检索文件,该文件将作为字符串提供。最后,它更喜欢异步工作,这在JavaScript中进行设置可能非常棘手,但是值得获得最佳加载时间。如果您想跳过这些麻烦事,可以将open()函数的第三个参数设置为false,但是浏览器会在控制台中向您投诉。
我们可以以与处理纹理相同的方式使用AJAX处理异步下载-添加is_loaded属性。我还将检查纹理是否已加载,因为如果先加载VAO并使用其他纹理进行渲染,它将看起来很糟糕:

var my_vao = start_loading_obj ("meshes/my_mesh.obj"); ... // _inside the main drawing loop_ if (my_vao.is_loaded && texture.is_loaded) { vao_ext.bindVertexArrayOES (my_vao); ... // draw stuff that requires the VAO }

我不会在此处粘贴50行左右的.obj解析文件,但是您可以在obj_parser.js上查看它。您可以采用多种方法来构造JavaScript对象和函数。 JavaScript的另一个烦人的局限性是,您不能像在C语言中那样真正地从一个函数中传回一个以上的变量。您可能会考虑创建一个包含VAO和点数的网格容器对象,或者只是将顶点点数添加为像我在这里一样,您的VAO中的另一个属性-这取决于您希望在工作代码中拥有多少抽象。

function start_loading_obj (url) { // first create an empty VAO var vao = vao_ext.createVertexArrayOES (); // inject an is_loaded boolean vao.is_loaded = false; // inject point count into VAO (yeah...) vao.pc = 0; var xmlhttp = new XMLHttpRequest(); xmlhttp.open ("GET", url, true); xmlhttp.onload = function (e) { var str = xmlhttp.responseText; var lines = str.split ('\n'); for (var i = 0; i < lines.length; i++) { //...parsing code goes here... } // store loaded state and point count in VAO vao.pc = sorted_vp.length / 3; vao.is_loaded = true; } // start loading xmlhttp.send (); // return the empty VAO return vao; }

请注意,类似于纹理创建,第一步是创建有效但为空的VAO。下载开始时,它将返回给函数调用者。这意味着,即使下载尚未完成,我们的主程序仍然对最终的VAO具有有效的句柄,并且可以检查其状态。发送功能开始HTTP握手。文件实际下载到客户端后,在原始函数调用返回后的某个时间onload回调函数将启动。在测试时很容易在这里犯一个错误。在本地网络上,下载不会有延迟。从另一个具有大网格的大陆,延迟可能会花费一些时间-在测试异步下载代码时,请检查可能最长和最坏的连接。您肯定会因很少的检查标志和回调而犯错误,并且拥有假定某些内容未经过充分检查就下载的代码。 在解析代码中,您可以使用str.split('\\ n');命令返回一个字符串数组;文件中的每一行一个,并使用for(var i= 0; i要包含外部JavaScript文件,我们只需添加另一个脚本块,然后指定src =“ path_to_file.js”属性即可。

我在这里有我文件的相对路径。请注意,您必须具有结束脚本标记-您不能像使用其他类型的HTML元素一样具有单个自闭合的标记。这个新的脚本块实际上应该出现在我们的其他块之前,以便浏览器在使用它之前对其进行解析。如果您不这样做,它仍然可以工作,但是浏览器可能会警告您,它被迫降低了脚本的加载效率。
跨域文件访问
如果您是从桌面加载网页,则AJAX可能会投诉并拒绝加载文件,因为它违反了安全策略。您可以将内容放在本地网络服务器上-有许多轻量级服务器可用。您可以使用--disable-web-security命令行标志启动Chrome以忽略此预防措施,也可以仅使用Firefox,Firefox应该完全忽略此问题并加载文件。
创建矩阵
您将需要一组用于JavaScript的向量和矩阵数学函数。布兰登·琼斯(Brandon Jones)的gl-matrix是一个非常受欢迎的库。我也将我的数学库移植到JavaScript。您当然也可以自己编写。仅将矩阵和向量保留为JavaScript的本机数组格式比创建自定义数据结构更容易。
值得一看的是JavaScript数学库的源代码,以了解函数,参数和数组在JavaScript中的工作方式。函数没有声明-只是定义。它们都以函数关键字而不是数据类型为前缀,并且可以返回或不返回值。参数只是给定名称,不需要var前缀。可以进行curry和更高级的功能编程。数组以方括号内的逗号分隔值列表形式给出。使用以下任一方法创建一个空数组

var my_array = []

要么

var my_array = new Array ().

我只在数学库中包含另一个脚本块。我使用外观熟悉的函数创建视图和投影矩阵:

var cam_dirty = true; var V = look_at ([0.0, 0.4, 1.0], [0.0, 0.4, 0.0], normalise_vec3 ([0.0, 1.0, 0.0])); var aspect = canvas.clientWidth / canvas.clientHeight; var P = perspective (67.0, aspect, 0.1, 100.0); var PV = mult_mat4_mat4 (P, V);

注意:我的画布尺寸是整数,但是这里的除法是浮点除法,而不是整数除法。这可能是您第一次接触到可怕的JavaScript。如果您想要特定的数据类型,通常必须使用特定的截断或解析函数来强制执行它-在大多数情况下,浮点数或字符串似乎是JavaScript中默认的一种假定数据类型。
画循环
要在JavaScript中循环,最好的策略是创建函数,将要循环的代码放入其中,并在完成时通知浏览器您要再次调用此函数。不幸的是,请求重复调用的实际功能似乎尚未在所有浏览器上标准化。我从Google的webgl-utils.js文件中摘录了一个片段,以将其封装在跨浏览器功能中:

window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, element) { return window.setTimeout (callback, 1000 / 60); }; })();

如果将其放在脚本块中的某个地方,我们可以调用它。我们还将编写一个重复函数来绘制图形。如果愿意,可以在启动代码之后输入:

var previous_millis; function main_loop () { // update timers var current_millis = performance.now (); var elapsed_millis = current_millis - previous_millis; previous_millis = current_millis; var elapsed_s = elapsed_millis / 1000.0; // draw gl.clear (gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.activeTexture (gl.TEXTURE0); gl.bindTexture (gl.TEXTURE_2D, texture); gl.useProgram (sp); if (cam_dirty) { gl.uniformMatrix4fv (heckler_PV_loc, gl.FALSE, new Float32Array (PV)); cam_dirty = false; } var R = rotate_y_deg (identity_mat4 (), current_millis * 0.075); gl.uniformMatrix4fv (heckler_M_loc, gl.FALSE, new Float32Array (R)); vao_ext.bindVertexArrayOES (heckler_vao); gl.drawArrays (gl.TRIANGLES, 0, heckler_vao.pc); // "automatically re-call this function please" window.requestAnimFrame (main_loop, canvas); }

我喜欢在循环中添加一些计时器,以便可以制作动画,测量帧速率等。您可以使用浏览器的performance.now()函数来执行此操作,这可以使您自页面加载后以毫秒为单位,系统的最高精度可达纳秒级-比JavaScript的数据和时间函数可靠得多。之后,我开始绘制代码。我更新了投影的矩阵统一形式,并查看是否尚未更新。矩阵的统一更新功能有些特殊之处。在WebGL中,必须将transposition参数设置为false-它不会为您进行转换。最好将矩阵数组强制为32位浮点数据类型,您可以通过创建一个新的Float32Array对象来实现。在函数的最后,我再次调用了请求调用函数。缓冲区交换和最终图像在画布上的实际绘制都是自动处理的。在脚本块的末尾,我们实际上可以调用此函数,它将开始循环过程:

previous_millis = performance.now(); main_loop ();

这些就是WebGL的基础知识,如果您之前已经做过一些OpenGL的话,这些知识就足够了。

进阶

浏览器具有许多您可以访问的功能。您可以查看用户与鼠标,键盘,移动设备的触摸屏,甚至是新的W3C游戏手柄/操纵杆界面的交互。您可以查看设置更复杂的异步文件流。
拥有来自HTML的一组GUI工具不容小under。您可以在图表,滑块,按钮上使用Web的所有附加功能,最重要的是,您可以进行文本渲染。如果您愿意通过CSS将其覆盖在画布上。您还可以将画布浮动在其他Web内容上,并使背景透明。
浏览器具有内置的调试,源,步进和监视列表工具。您可以在浏览器中进行一些非常全面的调试,甚至在应用程序运行时输入新的JavaScript代码。尝试这些工具是一个好主意。
请确保观看新的WebGL 2.0标准的开发,并查看您的浏览器是否已运行此版本的早期版本。