【转】使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介

转自HTML5游戏开发者社区

使用 WebGL 进行 3D 开发,第 1 部分: WebGL 简介
使用 WebGL 进行 3D 开发,第 2 部分: 使用 WebGL 库以更少的编码做更多的事情
使用 WebGL 进行 3D 开发,第 3 部分: 添加用户交互


 

WebGL API 让 JavaScript 开发人员能够直接利用如今的 PC 及移动设备硬件中强大的内置 3D 图形加速功能。现代浏览器透明地支持 WebGL,它使人们可以为主流 Web 用户创建高性能的 3D 游戏、应用程序以及 3D 增强的用户界面。本文是由三部分组成的系列文章中的第 1 部分,该系列面向刚刚接触 WebGL 的 JavaScript 开发人员。在这一部分中,我们将通过一个基本的示例来介绍 WebGL 的基础知识和相关的 3D 图形概念。

我们生活在一个 3D 世界中,但我们与计算机及计算机化设备的几乎所有交互都发生在 2D 用户界面上。直到最近,高速、流畅、逼真的 3D 应用程序(曾经是计算机动画师、科研用户和游戏爱好者的专属领域)对于主流 PC 用户还是遥不可及的。(见边栏:3D 硬件进化:简史。)

如今,所有主流 PC 的 CPU 都内置了 3D 图形加速,并且游戏 PC 有额外的专用高性能图形处理单元(GPU)来处理 3D 渲染。手机和平​​板电脑中基于精简指令集计算(RISC)的 CPU 反映了这一趋势。 目前所有的移动 CPU 都包括支持 3D 的强大图形加速 GPU。配套的软件驱动程序也日渐成熟,并且现在也更加稳定高效。

现代浏览器技术的进步为它们带来了硬件加速的 WebGL,这是一个与具有丰富特性的 HTML5 一起运行的 3D JavaScript API。JavaScript 开发人员现在可以创建交互式 3D 游戏、应用程序和 3D 增强的用户界面。由于 WebGL 被集成到主流浏览器中,只配备了浏览器和文本编辑器的大量开发人员终于可以进行 3D 应用程序开发了。

本文是由三部分组成的系列文章中的第 1 部分,主要介绍 WebGL。首先简要概述 3D 软件栈的演变。然后,您将有机会通过涵盖 WebGL 编程关键方面的动手示例来体验 WebGL API。(参见 下载 部分的示例代码。)该示例既全面又易于理解,并且伴随着解释了一些基本的 3D 图形概念。(假定您熟悉 HTML5 的 canvas 元素。)第 2 部分介绍高级 WebGL 库。在第 3 部分中,您可以将一切都融合在一起,开始创建引人注目的 3D 用户界面和应用程序。

3D 应用程序软件栈

在 PC 早期历史的大部分时间中,3D 硬件驱动程序都与应用程序捆绑,或被编译到应用程序中。该配置可以优化对硬件的硬件加速特性的访问,从而实现最佳性能。基本上,您可以直接编码实现硬件的功能。精心设计的游戏或计算机辅助设计(CAD)应用程序可以充分利用底层硬件。图 1 显示了该软件配置。

但捆绑的成本很昂贵。应用程序一经发布和安装,硬件驱动程序就如同被冻结一般(包括错误在内的一切)。如果图形卡供应商修复了一个错误或者推出了性能增强的驱动程序,应用程序用户必须安装或升级应用程序才可以利用它。此外,由于图形硬件在迅速发展,使用编译的 3D 驱动程序的应用程序或游戏很容易很快就过时。当新的硬件推出时(包含新的或更新后的驱动程序),软件供应商必须开发并发布新版本。在高速宽带网络普遍可访问之前,这是一个主要的分发问题。

作为驱动程序更新问题的解决方案,操作系统承担了托管 3D 图形驱动程序的角色。应用程序或游戏调用操作系统提供的一个 API,操作系统随后将调用转换为原生 3D 硬件驱动程序可接受的原语。图 2 展示了这一安排。

 

通过这种方式(至少在理论上),可以针对操作系统的 3D API 对应用程序进行编程。对 3D 硬件驱动程序的修改,甚至 3D 硬件本身的进化对于应用程序都是屏蔽的。对于许多应用程序而言,包括所有主流浏览器,这样的配置足以在很长一段时间内满足需求。操作系统充当中间人,试图大胆地迎合各种类型或样式的应用程序,以及迎合来自竞争厂商的图形硬件。但是,这种一刀切的方法会影响 3D 渲染性能。要求最佳硬件加速性能的应用程序仍然必须发现实际安装的图形硬件,实施调整,为每一组硬件优化代码,并往往根据厂商对操作系统的 API 的特定扩展进行编程,再次使应用程序受制于底层驱动程序或物理硬件。

WebGL 时代

进入现代社会,高性能的 3D 硬件已内置到每个桌面和移动设备中。人们越来越多地利用 JavaScript 开发应用程序,以利用浏览器功能,Web 设计师和 Web 应用程序开发人员强烈要求获得更快、更好的 2D/3D 浏览器支持。其结果是:主流浏览器厂商广泛支持 WebGL。

WebGL 以 OpenGL Embedded System (ES) 为基础,这是用于访问 3D 硬件的低级过程 API。OpenGL(由 SGI 在 20 世纪 90 年代初创建)现在被视为是一个易于理解且成熟的 API。WebGL 让 JavaScript 开发人员有史以来第一次能够以接近原生的速度访问设备上的 3D 硬件。WebGL 和 OpenGL ES 都在非营利组织 Khronos Group 的赞助下不断发展。

通过浏览器支持库和操作系统的 3D API 库,WebGL API 几乎可以直接访问底层的 OpenGL 硬件驱动程序,而无需首先转换代码。图 3 展示了这种新的模型。

硬件加速的 WebGL 支持浏览器上的 3D 游戏、实时 3D 数据可视化应用程序,以及未来的交互式 3D 用户界面(仅举几例)。OpenGL ES 的标准化确保可以安装新的厂商驱动程序,而不影响现有的基于 WebGL 的应用程序,并兑现理想化的 “在任何平台上支持任何 3D 硬件” 这一承诺。

动手体验 WebGL

现在是时候动手体验 WebGL 了。启动最新版本的 Firefox、Chrome 或 Safari,并从代码中打开 triangles.html(参见 下载 部分)。理想场景是通过一个 Web 服务器访问 HTML 页面,但在本例中,您也可以从您的文件系统打开它。(如果您直接打开 HTML 文件,本系列中后面的示例可能无法正常工作,因为可能需要通过 Web 服务器加载额外的图形数据。)页面看起来应该类似于图 4 中的屏幕截图,该页面是从运行于 OS X 之上的 Safari 打开的。

两个看似完全相同的蓝色三角形出现在页面上。然而,并非所有三角形的创建方式都一样。两个三角形都是使用 HTML5 canvas 绘制的。但是,左边的那个是 2D 图,并且用于绘制它的 JavaScript 代码不到 10 行。右边的那个是一个四面的 3D 金字塔对象,需要超过 100 行 JavaScript WebGL 代码来渲染。

如果查看网页的源代码,就可以确认有大量的 WebGL 代码绘制右边的三角形。然而,该三角形看起来肯定不是 3D 图形。(不是 3D 图形,戴上红蓝色 3D 眼镜也无济于事。)

WebGL 绘制 2D 视图

您在 triangles.html 的右侧看到一个三角形,这是因为金字塔的方向。您看到的是一个多色金字塔的蓝色的一面,类似于直视建筑物的一面,只看到一个 2D 矩形。(快速看一下 图 5,可以看到 3D 的金字塔。)此实现强调了在浏览器中使用 3D 图形的本质:最终的输出始终是一个 3D 场景的 2D 视图。 因此,通过 WebGL 进行的 3D 场景的任何静态渲染都是一个 2D 图像。

接下来,在您的浏览器中加载 pyramid.html。在此页面上,绘制金字塔所需的代码几乎与 triangles.html 中的代码完全一样。一个区别是,添加了一些代码,用于沿 y 轴连续旋转金字塔。换言之,以一定的时间延迟(使用 WebGL)相继绘制了相同 3D 场景的多个 2D 视图。随着金字塔旋转,可以清楚地看到,之前位于 triangles.html 右侧的蓝色三角形其实是一个多色 3D 金字塔的一面。图 5 显示了运行于 OS X 之上的 Safari 中的 pyramid.html 快照。

编写 WebGL 代码

清单 1 显示了 triangles.html 中的两个 canvas 元素的 HTML 代码。

清单 1. 包含两个 canvas 元素的 HTML 代码
?
1
2
3
4
5
6
7
8
9
10
11
12
< html >
< head >
...
head >
   < body onload = "draw2D();draw3D();" >
     < canvas id = "shapecanvas" class = "front" width = "500" height = "500" >
     canvas >
     < canvas id = "shapecanvas2" style = "border: none;" width = "500" height = "500" >
     canvas >
   < br />
   body >
html >

onload 处理程序调用了两个函数:draw2D() 和 draw3D()draw2D() 函数在左侧画布上 (shapecanvas) 绘制 2D 图形。draw3D() 函数在右侧画布上 (shapecanvas2) 绘制 3D 图形。

在左侧画布绘制 2D 三角形的代码如清单 2 所示。

清单 2. 在 HTML5 画布上绘制 2D 三角形
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function draw2D()  {
 
     var canvas = document.getElementById( "shapecanvas" );
     var c2dCtx = null ;
     var exmsg = "Cannot get 2D context from canvas" ;
     try {
       c2dCtx = canvas.getContext( '2d' );
     }
     catch (e)
     {
       exmsg = "Exception thrown:" + e.toString();
     }
     if (!c2dCtx) {
       alert(exmsg);
       throw new Error(exmsg);
     }
     c2dCtx.fillStyle = "#0000ff" ;
     c2dCtx.beginPath();
     c2dCtx.moveTo(250, 40);        // Top Corner
     c2dCtx.lineTo(450, 250);         // Bottom Right
     c2dCtx.lineTo(50, 250);         // Bottom Left
     c2dCtx.closePath();
     c2dCtx.fill();
}

在 清单 2 中简单直观的 2D 绘图代码中,绘图上下文 c2dCtx 从 canvas 元素获取而来。然后调用上下文的绘图方法,创建一组跟踪三角形的路径。最后,使用 RGB 颜色 #0000ff(蓝色)填充封闭路径。

获取 3D WebGL 绘图上下文

清单 3 显示,用于从 canvas 元素获取 3D 绘图上下文的代码与 2D 情况下的代码几乎一样。其区别是,要请求的上下文名称是 experimental-webgl,而不是 2d

清单 3. 从 canvas 元素获取 WebGL 3D 上下文
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function draw3D()  {
       var canvas = document.getElementById( "shapecanvas2" );
 
       var glCtx = null ;
       var exmsg = "WebGL not supported" ;
       try
       {
           glCtx = canvas.getContext( "experimental-webgl" );
       }
       catch (e)
       {
           exmsg = "Exception thrown:" + e.toString();
       }
       if (!glCtx)
       {
           alert(exmsg);
           throw new Error(exmsg);
       }
  ...

在清单 3 中,如果浏览器不支持 WebGL,draw3D() 函数会显示一个警报,并引发一个错误。在生产应用程序中,您可能会想使用更加特定于应用程序的代码来处理这种情况。

设置视口(viewport)

为了告诉 WebGL 渲染输出应该去哪里,您必须设置视口,方法是在 WebGL 可以绘图的画布内以像素为单位指定区域。在 triangles.html 中,整个画布区域都用于渲染输出:

?
1
2
// set viewport
  glCtx.viewport(0, 0, canvas.width, canvas.height);

在接下来的步骤中,您必须开始创建数据,馈送到 WebGL 渲染管道。该数据必须描述构成场景的 3D 对象。在本例中,场景仅仅是一个四面的多色金字塔。

描述 3D 对象

要为 WebGL 渲染描述 3D 对象,您必须使用三角形来表示对象。WebGL 采用的描述可以是一组离散的三角形的形式,或者是有共享顶点的三角形的一个条带。在金字塔的示例中,四面的金字塔用一组四个不同的三角形来描述。每个三角形由它的三个顶点指定。图 6 显示了金字塔其中一面的顶点。

在图 6 中,这一面的三个顶点是 y 轴上的 (0,1,0)、z 轴上的 (0,0,1) 和 x 轴上的 (1,0,0)。在金字塔本身,这一面是黄色的,在可以看见的蓝色一面的右侧。扩展同样的模式,您可以遵循此规则勾画出金字塔的其他三面。清单 4 中的代码在名为 verts 的数组中定义了金字塔的四个面。

清单 4. 描述组成金字塔的一组三角形的顶点数组
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Vertex Data
vertBuffer = glCtx.createBuffer();
glCtx.bindBuffer(glCtx.ARRAY_BUFFER, vertBuffer);
var verts = [
0.0, 1.0, 0.0,
-1.0, 0.0, 0.0,
0.0, 0.0, 1.0,
 
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
 
0.0, 1.0, 0.0,
1.0, 0.0, 0.0,
0.0, 0.0, -1.0,
 
0.0, 1.0, 0.0,
0.0, 0.0, -1.0,
-1.0, 0.0, 0.0
 
];
glCtx.bufferData(glCtx.ARRAY_BUFFER, new Float32Array(verts),
    glCtx.STATIC_DRAW);

注意,金字塔的底部(它实际上是 x-z 平面上的一个正方形)没有包含在 verts 数组中。因为金字塔绕 y 轴旋转,观看者永远看不到底部。在 3D 作品中不渲染观看者永远看不到的对象表面,这是惯例。保持不渲染它们,可以显著加快复杂对象的渲染。

在 清单 4 中,verts 数组中的数据被打包到一个二进制格式的缓冲中,3D 硬件可以高效地访问该缓冲。这都通过 JavaScript WebGL 调用完成:首先,使用 WebGLglCtx.createBuffer() 调用创建一个零大小的新缓冲区,并通过 glCtx.bindBuffer() 调用将它绑定到 OpenGL 级别的 ARRAY_BUFFER 目标。接下来,在 JavaScript 中定义要加载的数据值数组,glCtx.bufferData() 调用设置当前绑定的缓冲区的大小,并将 JavaScript 数据(首先将 JavaScript 数组转换成 Float32Array 二进制格式)打包到设备驱动程序缓冲区中。

其结果是一个 vertBuffer 变量,它引用包含所需顶点信息的硬件缓冲区。该缓冲区中的数据可以由 WebGL 渲染管道中的其他处理器直接高效地访问。

指定金字塔各个面的颜色

必须设置的下一个低级缓冲由 colorBuffer 引用。此缓冲中包含金字塔的每一面的颜色信息。在该示例中,颜色是蓝、黄、绿和红。清单 5 显示了如何设置 colorBuffer

清单 5. colorBuffer 设置指定金字塔的每一面的颜色
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
colorBuffer = glCtx.createBuffer();
         glCtx.bindBuffer(glCtx.ARRAY_BUFFER, colorBuffer);
         var faceColors = [
             [0.0, 0.0, 1.0, 1.0], // front  (blue)
             [1.0, 1.0, 0.0, 1.0], // right  (yellow)
             [0.0, 1.0, 0.0, 1.0], // back   (green)
             [1.0, 0.0, 0.0, 1.0], // left   (red)
         ];
         var vertexColors = [];
         faceColors.forEach( function (color) {
             [0,1,2].forEach( function () {
               vertColors = vertColors.concat(color);
             });
         });        glCtx.bufferData(glCtx.ARRAY_BUFFER,
          new Float32Array(vertexColors), glCtx.STATIC_DRAW);

在清单 5 中,通过 createBuffer()bindBuffer() 和 bufferData() 调用来设置低级 colorBuffer 缓冲,这些函数与为 vertBuffer 所使用的那些函数完全一样。

但 WebGL 没有金字塔 “面” 这一概念。相反,它仅使用三角形和顶点。颜色数据必须与顶点相关联。在 清单 5 中 ,名为 faceColors 的一个中间 JavaScript 数组初始化 vertColors 数组。 vertColors 是在加载低级 colorBuffer 时所使用的 JavaScript 数组。faceColors 数组包含 4 种颜色(蓝、黄、绿和红),分别对应于四个面。 这些颜色以红、绿、蓝、Alpha (RGBA) 格式指定。

vertColors 数组包含每一个三角形的每个顶点的颜色,其顺序对应于它们在 vertBuffer 中出现的顺序。因为四个三角形中每一个都有三个顶点,最终的 vertColors 数组包含总共 12 个颜色条目(其中每一个条目都是由 4 个 float 数字构成的数组)。使用一个嵌套的 forEach 循环将相同的颜色分配给代表金字塔一面的每个三角形的三个顶点。

了解 OpenGL 着色器(shaders)

可能会自然地浮现在脑海中的一个问题是,指定一个三角形的三个顶点的颜色如何能够用该颜色渲染整个三角形。要回答这个问题,您必须了解 WebGL 渲染管道中两个可编程组件的操作:顶点着色器 和片段(像素)着色器。可以将这些着色器编译成能够在 3D 加速硬件 GPU 上执行的代码。一些现代的 3D 硬件可以并行执行数百个着色器操作,实现高性能的渲染。

顶点着色器处理每个指定的顶点。着色器接受的输入包括颜色、位置、纹理以及与顶点相关联的其他信息。然后,着色器计算和转换数据,以确定在应渲染该顶点的视口上的 2D 位置,以及顶点的颜色和其他属性。片段着色器确定在顶点之间组成三角形的每个像素的颜色和其他属性。使用 OpenGL Shading Language (GLSL) 通过 WebGL 对顶点着色器和片段着色器进行编程。

GLSL

GLSL 是一种编程语言,其语法类似于 ANSI C(有一些 C++ 的概念)。它是特定于域的,支持从可用的对象形状、位置、角度、颜色、照明、纹理,以及其他相关信息映射到将要渲染 3D 对象的每个 2D 画布像素所显示的实际颜色。

关于使用 GLSL 编写自己的着色器程序的细节已超出本文的范围。但您需要对 GLSL 代码有最基本的认识才可以理解该示例程序的其余部分。我将指导您完成本例中使用的这两个普通 GLSL 着色器的操作,以帮助您理解有关它们的所有代码。

在本系列的下一篇文章中,您将学习如何使用更高级别的 3D 库和框架来与 WebGL 配合。这些库和框架透明地融入了 GLSL 代码,所以您可能永远都不需要自己编写一个着色器。

在 WebGL 中处理着色器程序

着色器程序是相关着色器(在 WebGL 中通常是顶点着色器和片段着色器)的一个链接的二进制文件,随时可供硬件 GPU 执行。每个着色器可以有几乎微不足道的一行代码,也可以有数百行高度复杂且多特性的并行代码。

在通过 WebGL 执行着色器之前,必须将程序的 GLSL 源代码编译成二进制代码,然后链接在一起。厂商提供的 3D 驱动程序嵌入了编译器和链接器。 您必须通过 JavaScript 提交 GLSL 代码,检查编译错误,然后链接准备作为参数的矩阵。WebGL 有一个 API 可用于所有这些操作。图 7 展示了通过 WebGL 提交 GLSL 代码的序列。

用于获取、编译和链接该示例的 GLSL 着色器的代码如清单 6 所示。

清单 6. 在 WebGL 中编译和链接 GLSL 着色器代码
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var vertShaderCode = document.getElementById( "vertshader" ).textContent;
var fragShaderCode = document.getElementById( "fragshader" ).textContent;
 
var fragShader = glCtx.createShader(glCtx.FRAGMENT_SHADER);
 
glCtx.shaderSource(fragShader, fragShaderCode);
glCtx.compileShader(fragShader);
 
if (!glCtx.getShaderParameter(fragShader, glCtx.COMPILE_STATUS)) {
    var errmsg = "fragment shader compile failed:"
     + glCtx.getShaderInfoLog(fragShader);
    alert(errmsg);
    throw new Error()
   }
 
var vertShader = glCtx.createShader(glCtx.VERTEX_SHADER);
 
glCtx.shaderSource(vertShader, vertShaderCode);
glCtx.compileShader(vertShader);
 
if (!glCtx.getShaderParameter(vertShader, glCtx.COMPILE_STATUS)) {
    var errmsg = "vertex shader compile failed :"
        + glCtx.getShaderInfoLog(vertShader);
    alert(errmsg);
    throw new Error(errmsg)
   }
 
// link the compiled vertex and fragment shaders
shaderProg = glCtx.createProgram();
glCtx.attachShader(shaderProg, vertShader);
glCtx.attachShader(shaderProg, fragShader);
glCtx.linkProgram(shaderProg);

在清单 6 中,顶点着色器的源代码被存储为 vertexShaderCode 中的一个字符串,而片段着色器的源代码存储在 fragmentShaderCode 中。借助 document.getElementById().textContent 属性,从 DOM 中的 

你可能感兴趣的:(javascript,操作系统,游戏,ViewUI)