HTML5-WebGL-入门指南-全-

HTML5 WebGL 入门指南(全)

原文:Beginning WebGL for HTML5

协议:CC BY-NC-SA 4.0

零、简介

webGL(基于 Web 的图形语言)是一种令人兴奋的新技术,它可以让你在 Web 浏览器中创建强大的 3D 图形。实现这一点的方法是使用与图形处理单元(GPU)交互的 JavaScript API。这本书将很快让你了解着色器和渲染现实场景。为了确保愉快的开发,我们将展示如何使用调试工具和调查库来最大化生产力。

观众

面向 HTML5 的入门 WebGL 面向具有计算机图形技术基础知识的图形爱好者。了解 OpenGL,尤其是使用可编程管道的版本,如 OpenGL ES,是有益的,但不是必需的。我们将浏览所有相关材料。JavaScript 背景肯定会有帮助。

写这种性质的书时,我们很遗憾不能涵盖所有的先决材料。需要做出关于读者的基本假设。我所做的假设是,读者对 2D 和 3D 计算机图形概念有基本的了解,如像素、颜色、图元和变换。附录 B 快速更新了这些概念。还假设读者熟悉 HTML、CSS 和 JavaScript(尽管不一定是专家)。尽管这本书的大部分内容使用了普通的“香草”JavaScript,我们还是会使用一些 jQuery。附录 A 讨论了较新的 HTML5 概念和一个快速 jQuery 速成班,这对正确理解本文是必不可少的。附录 D 为整本书中出现的主题的进一步阅读提供了完整的参考。

你将学到什么

这本书在必要的时候提供理论,在可能的时候提供例子。您将对 WebGL 有一个很好的了解。您将学到以下内容:

  • 了解模型视图矩阵并设置场景
  • 渲染和操纵图元
  • 了解着色器,热爱它们的强大功能和灵活性
  • 探索创造真实场景的技巧
  • 用基础物理来模拟互动
  • 使用数学模型渲染粒子系统、地形和分形
  • 提高现有模型、着色器和库的工作效率
  • 使用 Three.js 框架
  • 了解 GLGE 和 philoGL 框架以及其他可用框架的调查
  • 调试和性能提示
  • 了解着色器的其他用途,如图像处理和非真实感渲染
  • 使用备用帧缓冲区实现拾取和阴影贴图
  • 了解当前的浏览器和移动支持以及 WebGL 的未来

页状构造

建议您先阅读前两章,然后再阅读本书的其他部分。即使这本书遵循了一个相当自然的进程,你也可以选择按顺序阅读或者随意跳过。例如,第九章的调试部分严格来说并不是必不可少的,但却是尽快了解的非常有用的信息。

第一章:场景设置

我们将介绍使用 WebGL 渲染图像的所有步骤,包括使用顶点缓冲对象(vbo)和基本着色器测试浏览器支持和设置 WebGL 环境。我们从创建一个单色的静态 2D 图像开始,到本章结束时,我们有了一个多种颜色的移动三维网格。

第二章:着色器 101

着色器将深入介绍。我们展示了图形管道(固定和可编程)的概述,给出了 GL 着色语言(GLSL)的背景,并解释了顶点和片段着色器的作用。接下来,我们将介绍 GLSL 的基本类型和语言细节,以及我们的 WebGL 应用将如何与我们的着色器进行交互。最后,我们展示几个 GLSL 用法的例子。

第三章:纹理和照明

我们展示了如何应用纹理和简单的照明。我们解释了纹理对象以及如何设置和配置它们,并在我们的着色器中将纹理查找与光照模型相结合。

第四章:增强现实主义

解释并实现了一个更真实的光照模型——Phong 照明。我们讨论平面和平滑着色以及顶点和片段计算之间的区别。我们展示了如何添加雾和混合对象;并讨论阴影、全局照明、反射和折射。

第五章:物理

本章展示了如何对重力、弹性和摩擦力建模。我们探测碰撞并对其做出反应,模拟射弹并探索动量守恒、势能守恒和动能守恒。

第六章:分形、高度图和粒子系统

在这一章中,我们将展示如何直接使用 GPU 进行绘画,讨论分形,并对 Mandlebrot 和 Julia 集进行建模。我们还展示了如何从一个纹理生成一个高度图,并生成地形。我们也探索粒子系统。

第七章:Three.js 框架

Three.js WebGL 框架介绍。我们提供了该库的背景和示例用法,包括如何在必要时返回到 2D 渲染上下文,API 调用来轻松创建相机、对象和照明。我们将早期的书籍示例与等效的 Three.js API 调用进行比较,并介绍 tQuery,这是一个结合了 Three.js 和 jQuery 选择器的库。

第八章:生产力工具

我们首先讨论使用框架的好处和学习核心 WebGL 的好处。讨论了几种可用的框架,并给出了 GLGE 和 philoGL 框架的例子。我们展示了如何加载现有的网格和寻找其他资源。我们列出了可用的物理库,并以一个使用 physi.js 库的例子结束了本章。

第九章:调试和性能

这是一个重要的章节,通过遵循已知的 WebGL 最佳实践,帮助识别和修复错误代码并提高性能。

第十章:效果、技巧和诀窍

讨论并实现了图像处理和非真实感着色器。我们展示了如何使用屏幕外帧缓冲区,使我们能够从画布上拾取对象并实现阴影贴图。

后记:WebGL 的未来

在后记中,我们将推测 WebGL 的光明前景,当前在浏览器和移动设备中对它的采用以及接下来将添加哪些功能。

附录 A:基本 HTML5 和 JavaScript

我们讨论了 HTML 4 和 5 之间的一些变化,比如更短的标签、增加的语义文档结构、

元素以及基本的 JavaScript 和 jQuery 用法。

附录 B:图形刷新程序

这个附录是一个图形复习,包括坐标系,初等变换和其他基本主题。

附录 C: WebGL 规范零零碎碎

包含 WebGL 规范的一部分,可在www.khronos.org/registry/webgl/specs/latest/获得,这本书没有涉及到,但仍然很重要。

附录 D:附加资源

一个参考列表,用于进一步阅读书中的主题,如 HTML5、WebGL、WebGLSL、JavaScript、jQuery、服务器堆栈、框架、演示等等。

WebGL 原点

WebGL 的起源始于 20 年前,当时 OpenGL 的 1.0 版本是作为 Silicon Graphics 的 Iris GL 的非专有替代品发布的。直到 2004 年,OpenGL 一直使用固定功能管道(在第二章中有解释)。那年发布了 OpenGL 2.0 版本,引入了 OpenGL 着色语言(GLSL),让你可以对流水线的顶点和片段着色部分进行编程。OpenGL 的当前版本是 4.2,但是 WebGL 是基于 OpenGL 嵌入式系统(ES) 2.0 的,后者于 2007 年发布,是 OpenGL 2.0 的微调版。

因为 OpenGL ES 是为在嵌入式设备(如移动电话)中使用而构建的,与台式计算机相比,移动电话具有较低的处理能力和较少的功能,所以它比 OpenGL 具有更大的限制性和更小的 API。例如,使用 OpenGL,您可以使用 glBegin…glEnd 截面或 VBOs 来绘制顶点。OpenGL ES 只使用 VBOs,这是性能最友好的选项。大多数在 OpenGL 中可以完成的事情在 OpenGL ES 中也可以完成。

2006 年,Vladimar Vukic evic 开发了一个 Canvas 3D 原型,它使用了 OpenGL。2009 年,Khronos 小组创建了 WebGL 工作组,并开发了一个中央规范,有助于确保跨浏览器的实现相互接近。3D 上下文被修改为 WebGL,规范的 1.0 版本于 2011 年春完成。WebGL 规范的开发正在积极进行中,最新版本可在www.khronos.org/registry/webgl/specs/latest/找到。

WebGL 是如何工作的?

WebGL 是一个从 CPU 绑定到计算机显卡 GPU 的 JavaScript API。API 上下文从 HTML5

元素获得,这意味着不需要浏览器插件。着色器程序使用 GLSL,这是一种类似 C++的语言,在运行时编译。

在没有框架的情况下,设置 WebGL 场景确实需要相当多的工作:处理 WebGL 上下文、设置缓冲区、与着色器交互、加载纹理等等。使用 WebGL 的好处是它比 2D 画布环境快得多,并且能够产生一定程度的真实感和可配置性,这在使用 WebGL 之外是不可能的。

使用

WebGL 的一些用途是查看和操纵模型和设计、虚拟旅游、地图绘制、游戏、艺术、数据可视化、创建视频、操纵和处理数据和图像。

示范

WebGL 有许多演示,包括:

支持的环境

你的浏览器支持 WebGL 吗?重要的是要知道,目前并非所有浏览器、计算机和/或操作系统(OS)都支持 WebGL。浏览器支持是最容易满足的要求,只需升级到新版本的浏览器,或者在必要时切换到支持 WebGL 的不同浏览器即可。最低要求如下:

  • 火狐 4+
  • Safari 5.1+(仅限 OS X)
  • 铬 9+
  • Opera 阿尔法+
  • internet Explorer(IE)—无本地支持

虽然 IE 目前没有内置支持,但是插件是有的;比如 JebGL(在code.google.com/p/jebgl/有售)、Chrome Frame(在www.google.com/chromeframe有售)、ie webgl(iewebgl.com/)。JebGL 将 WebGL 转换为 Java applet,用于有缺陷的浏览器;Chrome Frame 允许在 IE 上使用 WebGL,但要求用户在客户端安装它。同样,IEWebGL 是一个 IE 插件。

除了当前的浏览器,您还需要支持的操作系统和更新的显卡。还有一些显卡和操作系统组合存在已知的安全漏洞,或者非常容易出现严重的系统崩溃,因此默认情况下会被浏览器列入黑名单。

Chrome 在以下操作系统上支持 WebGL(根据谷歌 Chrome 帮助(www.google.com/support/chrome/bin/answer.py?答案=1220892):😃

  • Windows Vista 和 Windows 7(推荐)没有 2009 年 1 月之前的驱动程序
  • Mac OS 10.5 和 Mac OS 10.6(推荐)
  • Linux 操作系统

通常,将图形驱动程序更新到最新版本会启用 WebGL。回想一下,OpenGL ES 2.0 是基于 OpenGL 2.0 的,所以这是您的显卡应该支持 WebGL 使用的 OpenGL 版本。还有一个叫 ANGLE(几乎是原生图形层引擎)的项目,讽刺的是用微软 Direct X 来增强一个图形驱动,通过转换成 Direct X 9 API 调用来支持 OpenGL ES 2.0 API 调用。结果就是只支持 OpenGL 1.5 (OpenGL ES 1.0)的显卡依然可以运行 WebGL。当然,对 WebGL 的支持在接下来的几年里应该会有很大的提高。

测试 WebGL 支持

检查浏览器对 WebGL 的支持。有几个网站如get.webgl.org/,在成功上显示一个旋转的立方体;和 http://doesmybrowsersupportwebgl.com/的,如果支持 webgl 上下文,它会给出一个很大的“是”或“否”以及具体的细节。我们还可以使用 modernizr(【http://www.modernizr.com】)以编程方式检查 WebGL 支持。

伙伴网站

除了 http://www.apress.com/9781430239963 的 press 网页,这本书在 http://www.beginningwebgl.com还有一个伙伴网站。这个网站展示了书中的例子,并提供了一个直接向作者提出评论和建议的地方。我们欢迎并感谢您的建设性反馈。

下载代码

本书中所示示例的代码可从 Apress 网站www.apress.com获得。在这本书的信息页上可以找到链接,在源代码/下载标签下的【http://www.apress.com/9781430239963】的。该选项卡位于页面相关标题部分的下方。更新后的代码也将在 https://github.com/bdanchilla/beginningwebgl的 github 上发布。

联系作者

如果你有任何问题或意见——或者甚至发现了你认为我应该知道的错误——你可以直接通过 bdanchilla@gmail.com 联系作者,或者通过www.beginningwebgl.com/contact的联系表格联系作者。

一、设置场景

在这一章中,我们将经历创建一个用 WebGL 渲染的场景的所有步骤。我们将向您展示如何

  • 获取 WebGL 上下文
  • 在 WebGL 中创建不同的原语类型
  • 理解和创建顶点缓冲对象(vbo)和属性
  • 做静态二维渲染
  • 创建程序和着色器
  • 设置视图矩阵
  • 添加动画和运动
  • 渲染三维模型

一张空白的画布

让我们首先创建一个带有单个

元素的 HTML5 文档(参见清单 1-1 )。

清单 1-1。 一张基本空白的画布

一张空白的画布

二、着色器 101

在这一章,我们将深入探讨 GL 着色语言(GLSL)。我们将涉及的主题包括

  • WebGL 图形管道概述
  • 固定功能和现代可编程着色器之间的区别
  • GLSL 中顶点着色器和片段着色器的作用
  • 如何在 WebGL 应用中创建和使用着色器
  • GLSL 的详细概述,包括其原始类型和内置函数
  • 程序片段着色器的示例

图形管道

一个图形管道由图像从初始定义到最终屏幕渲染的步骤组成。这个管道由按照预先定义的顺序完成的几个步骤组成。流水线的组件可以是功能固定的,也可以是可编程的。

固定功能或可编程着色器

更传统的图形流水线具有固定的实现。初始图像定义将是一组顶点位置点和与这些点相关联的信息,例如颜色、法向量和纹理坐标。对于固定功能,操作是按照设定的顺序完成的。您可以禁用一些元素,如照明或纹理,但不能修改基础照明或纹理计算的完成方式。OpenGL 版之前的图形管道只使用固定功能。

固定功能性、顾名思义,相当死板。它允许更快和更容易地生成图像,因为照明公式和阴影已经内置到系统中。然而,它限制了我们所能完成的,因为我们不能覆盖这些设置。OpenGL 固定功能为顶点变换和光照提供了单独的流水线步骤。现在这一切都在顶点着色器(VS)和片段着色器(FS)中完成。类似地,纹理应用、颜色叠加、雾化和 alpha 测试都是不连续的步骤。现在这些组件都在 FS 中完成了。

在图 2-1 中显示了 WebGL API、可编程和不可编程的管道组件如何交互的高级视图。

9781430239963_Fig02-01.jpg

图 2-1 。WebGL 可编程流水线的简图。带有阴影背景的步骤是可编辑的

可编程管道可以显示更大范围的效果,因为你可以定义管道的一部分(不是全部)并覆盖用于计算颜色、位置、纹理坐标或照明模型的计算。可编程流水线组件使用顶点程序和片段程序,它们统称为着色器。这些着色器运行在现代计算机中强大的图形处理单元(GPU)上。OpenGL 版本 2.0 到 3.0 允许使用固定功能或着色器。OpenGL ES 和 WebGL 的精简 API 只支持着色器,不支持固定功能。

为什么是着色器?

如果着色器需要更多的工作来设置,为什么我们还要费事去使用它们呢?它们的好处是什么?

嗯,使用着色器,您可以创建增加场景真实感的效果。您可以创建看起来很卡通的非真实感图像。也可以在着色器中创建卷积滤镜和遮罩;并在着色器中进行额外的抗锯齿、混合、阴影创建和高级纹理操作,以及几乎所有您能想到和实现的其他操作。

还可以对图形处理单元(GPU)进行编程,进行侧面计算。GPU 的能力可以用来抵消浏览器的计算,对于一般计算来说更快更好。

WebGL 图形管道

在 WebGL 中,渲染过程如下:

  • 获取顶点数组数据并将其放入顶点缓冲对象(VBOs)中。
  • 将 VBO 数据流式传输到 VS,并使用对具有隐式索引排序的 drawArrays 或具有 drawElements 和索引数组的 draw arrays 的调用来发送索引信息。
  • VS 运行,最低限度地设置每个顶点的屏幕位置,并可选地执行额外的计算,然后传递给 FS。
  • VS 的输出数据继续沿着流水线的固定部分向下传输。
  • GPU 使用顶点和索引生成图元。
  • 光栅化器丢弃位于视口之外的任何图元部分。然后视口内的部分被分解成像素大小的片段。
  • 然后在每个片段上内插顶点值。
  • 具有这些插值的片段被传递到 FS。
  • FS 最低限度地设置颜色值,但也可以做纹理和照明操作。
  • 片段可以被丢弃或传递到帧缓冲区,帧缓冲区存储 2D 图像,也可以选择使用深度和模板缓冲区。在这种情况下,深度测试和模板测试可以在最终图像中丢弃一些被渲染的片段。该图像或者被传递到绘图缓冲区并显示给用户,或者被保存到屏幕外缓冲区供以后使用,例如保存为纹理数据。

WebGL 渲染过程的高级视图如图 2-2 所示。

9781430239963_Fig02-02.jpg

图 2-2 。WebGL 渲染流程概述

在图 2-2 中,我们从模型坐标空间中的顶点位置开始。然后,VS 将顶点转换到最终位置。形成适当的图元类型,对图像进行剪裁、光栅化,并传递给 FS。FS 对值进行插值,并可选地通过深度和模板缓冲区发送结果,最后通过帧缓冲区。

GL 阴影语言

学习 GL 着色语言(GLSL)对于学习 WebGL 是必不可少的。我喜欢引用 Khronos WebGL wiki,它恰当地指出:

"没有着色器,WebGL 中不会发生任何事情。

背景

在 WebGL 中使用的着色语言实际上是 OpenGL ES 着色语言(也称为 GLSL ES 或 ESSL),并且基于 OpenGL 着色语言(GLSL)1.20 版。OpenGL ESSL 的完整规范可以从www . khronos . org/registry/gles/specs/2.0/GLSL _ ES _ Specification _ 1 . 0 . 17 . pdf下载。

GLSL 是基于 C++的,实际上是顶点和片段处理器的两种独立但紧密相关的语言。每个处理器上的编译源分别称为 VS 或 FS。VS 和 FS 链接在一起形成一个运行在 GPU 上的程序。

VS 一次作用于一个顶点,每个顶点可以有不同的属性与之关联。FS 作用于光栅化图像的一部分,并且可以内插顶点数据。它不能改变片段的位置或查看相邻片段的数据。VS 可以向 FS 发送数据。着色器程序的最终目标是更新帧(绘图)缓冲区或纹理缓冲区。

WebGL 使用 JavaScript 脚本语言将我们的着色器绑定到 GLSL 应用编程接口(API)。意识到我们在一个

  • 将同一 web 文件中的 VS 和 FS 源分别嵌入到“x 着色器/x 顶点”或“x 着色器/x 片段”类型的
  • 将 VS 和 FS 放在外部文件中,并用 Ajax 加载它们

image 注意默认情况下,<脚本>标签将 type 属性设置为 javascript 或 text/javascript。类型“x 着色器/x 顶点”和“x 着色器/x 片段”实际上不被浏览器识别并被忽略。该内容仍然被加载到文档对象模型(DOM)中以供以后检索,但在其他情况下不会被使用。

我们将在本章后面回到 GLSL。现在,让我们讨论着色器的角色。

着色器角色

VS 和 FS 具有不同的角色,它们一起工作来渲染完成的图像。本质上,VS 作用于每个顶点并负责设置最终的顶点位置,而 FS 作用于每个像素并设置最终的颜色。

顶点着色器

VS 负责所有的顶点坐标变换。这包括模型视图和投影矩阵视图计算。它还计算法线向量和纹理坐标的生成和转换。VS 可以执行逐顶点光照计算,并将这些值传递给 FS 进行逐像素计算。

总之,VS 负责

  • 最终顶点位置以及可选地
  • 逐顶点法线、纹理、照明和颜色
  • 将值传递给 FS

最低限度,一个 VS 需要设置 gl_Position,我们将在本章后面讨论,它是一个内置的 VS 变量(见清单 2-1 )。

清单 2-1 。 简单的顶点着色器,将输入的顶点位置传递给片段着色器

三、纹理和灯光

在这一章中,我们将讨论两个对于产生真实场景至关重要的主题,纹理和照明。具体来说,我们将

  • 讨论什么是纹理以及如何应用它们
  • 展示可用的纹理选项以及如何配置这些选项
  • 在着色器中使用多个纹理
  • 展示一个基本的照明模型
  • 创建平行光着色器

在本章结束时,我们将在图 3-1 的右边制作纹理和光照网格。

9781430239963_Fig03-01.jpg

图 3-1 。左-没有纹理或照明;右-纹理和照明

图 3-1 中的左图是我们为什么需要使用纹理和照明的一个具体例子。在第一章的最后一个例子中,一个三角形网格作为一个 3D 图形可见。它看起来是三维的原因仅仅是因为顶点颜色是不同的,并且是由我们的片段着色器插入的。这为我们提供了深度线索。正如您所看到的,当所有的顶点都具有相同的颜色,并且没有应用光照或纹理时,图像看起来像一个平面的二维多边形。其实还是 3D 的;它看起来很平的原因是没有上下文线索让我们知道这实际上是一个坚实的数字。

当我们看一幅图像时,我们依赖于一些线索,例如一个固体表面在光照方面的变化:黑暗/照明、反射、阴影和纹理的方向图案变化,来告诉我们一个表面在哪里结束,另一个表面在哪里开始。在图 3-1 右边的图像中,我们加入了纹理和光照线索,你可以清楚地分辨出这是一个立体。

口感

纹理是在我们的程序中应用于表面的图像。用作纹理的图像可以在原点进行位图化,也可以通过程序生成。纹理必须应用(映射)到我们的图像上,这样做通常会被拉伸、缩放、扭曲和/或重复。

纹理的宽度和高度通常是相同的,并且是 2 的幂,2 n ,例如 64、128、256 和 512。纹理的每个基本元素被称为一个纹理元素,代表纹理真实 el 元素或纹理真实像素 el

纹理坐标

在二维中,纹理坐标是以(s,t)对而不是像顶点位置那样的(x,y)对来引用的。通常,纹理坐标也被限制在(0,0)到(1,1)的范围内。对于 128x128 像素的纹理大小,所有点将被除以 128,以便位于该范围内。128x128 纹理的纹理坐标(0.5,0.25)指的是纹理元素(64,32)。

图 3-2 左边是源图像的坐标,右边是等效的纹理坐标。

9781430239963_Fig03-02.jpg

图 3-2 。左图-一个带有顶点坐标的 128×128 像素的正方形图像;右-等效纹理坐标

纹理坐标通常作为顶点属性值发送给着色器程序,但是(正如我们在上一章看到的)我们也可以在着色器程序中操纵它们。

纹理对象

在 WebGL 中,纹理存储在 WebGLTexture 对象中。要创建和绑定 WebGLTexture 对象,使用的 API 函数有:

webgltexture createtexture():

void bindTexture(GLenum 目标,WebGLTexture 纹理);

2D 纹理的目标将是 2D 纹理。其他目标类型在附录 C 中列出。

创建和绑定 WebGLTexture 的代码如下所示:

var texture = GL . create texture();

gl.bindTexture(gl)。2D 纹理,纹理:

要检查某个纹理是否正确加载,可以使用 API 调用:

glboolean istexture 纹理:

检查纹理的代码如下所示:

如果(!gl.isTexture(纹理) )

{

console.log("错误:纹理无效");

}

检查这一点很重要,因为如果当前没有绑定 WebGLTexture(通过向 bindTexture 传递 null 或 0),那么对纹理的进一步操作尝试将产生 INVALID_OPERATION 错误。

当您完成一个纹理时,您可以通过调用以下命令来删除它:

请参阅〈删除材质〉。

它看起来会像这样:

gl.deleteTexture(纹理):

现在我们已经初始化了一个 WebGLTexture 对象,我们准备向其中加载数据。

二维经纬仪〔??〕

将数据加载到纹理中的 API 调用是 texImage2D 函数。这个函数有五个签名变量。前四个是这种形式:

void textimage 2d(GLenum 目标,闪烁级别,GLenum 内部格式,

GLenum 格式,GLenum 类型,【来源】);

在此代码中,[source]可以是 ImageData、HTMLImageElement、HTMLCanvasElement 或 HTMLVideoElement 之一。后三个可能会抛出一个 DOMException。

调用的另一种形式是从类型化数组中指定数据:

void textimage 2d(GLenum 目标,闪烁级别,GLenum 内部格式,

格勒齐瓦宽度格勒齐瓦高度格勒齐瓦边界格勒姆格式,

GLenum 类型,ArrayBufferView?像素;

在第六章的中可以找到这种形式函数的使用示例。

“级别”参数指的是在小中见大贴图中使用的细节级别,这将在本章后面讨论。该参数通常设置为 0。内部格式和格式通常是 RGBA。并且类型往往是 UNSIGNED_BYTE。所有可用的格式和类型如附录 C 所示。

将图像载入纹理对象

填充纹理数据最常见的方法是从图像文件中填充。我们还可以设置数据或使用其他对象,如 HTMLCanvasElement 或 HTMLVideoElement。

我们将声明一个变量来保存我们的纹理图像数据:

var textureImage = null:

我们使用一个 HTML 图像对象来加载我们的纹理图像:

函数 loadTexture()

{

texture Image = new Image();

texture image . onload = function(){

setup texture();

}

textureImage.src = "。/textures/smiley-128 px . jpg ";

}

在 loadTexture 方法中,我们创建一个 HTML 图像对象并设置 onload 事件。这样做的目的是等待图像通过 textureImage.src 赋值被加载,然后调用 setupTexture 方法。我们的纹理设置的细节显示在列表 3-1 中。

image 注意我们将图像存储在 textureImage 变量中,而不是保存 WebGLTexture 对象的 texture 变量中。

清单 3-1 。?? 设置 WebGLTexture 对象

函数 setupTexture()

{

纹理= GL . create texture();

gl.bindTexture(gl)。2D 纹理,纹理:

GL . pixel tori(GL . un CK _ flip _ y _ webgl,true):

gl .二维顶点消除(gl)。2D 纹理,0,gl。RGBA、gl。RGBA、gl。SIGNED_BYTE,textureImage:

gl.texParameteri(gl。纹理 _2D,德国。纹理 _ 放大 _ 过滤,gl。最近);

gl.texParameteri(gl。纹理 _2D,德国。纹理最小过滤器。最近);

如果(!gl.isTexture(纹理) )

{

console.log("错误:纹理无效");

}

}

在清单 3-1 的纹理设置方法中,我们创建了一个 WebGLTextureObject,然后绑定它。然后,我们通过使用加载的 HTML 图像对象调用 texImage2D 来设置纹理数据。pixelStorei 函数告诉 WebGL 如何存储我们的数据,texParameteri 设置如何处理纹理过滤和包装的选项。我们将在本章后面更详细地介绍这两个新功能。最后,我们检查我们的纹理对象是否有效,如果无效,就向控制台输出一条错误消息。

image 注意这只是加载图像数据的一种方式。您也可以在现有的< img >标签中使用该图像:

函数 loadTexture()

{

textureImage = $("#smiley-image ")。get(0);

setup texture();

}

也可以使用 HTMLCanvasElement 或 HTMLVideoElement 中的图像,或者加载原始数据作为纹理图像。

纹理图像也必须遵循跨源资源共享(CORS) 的规则。如果你的纹理源和你的 JavaScript 文件在同一个位置,你不需要担心 CORS。更多关于 CORS 的确切限制的信息可以在www.w3.org/TR/cors找到,更严格的 WebGL CORS 限制可以在www.khronos.org/registry/webgl/specs/latest/#4.2找到

应用和着色器交互

我们需要从我们的应用发送我们加载的纹理对象到着色器程序。在我们的 setupTexture 函数中,我们将添加代码来获取 uSampler 制服的位置,并设置它的值以供我们的程序使用。

GL program . sampleruniform = GL . getuniformlocation(GL program," uSampler ");

GL . uniform 1 I(GL program . sample runiform,0);

第二个参数 0 指的是当前绑定的 TEXTURE0 纹理单元。TEXTURE0 是默认的纹理单位。

对于此示例,我们将使用这些数据点定义由两个三角形组成的平面的顶点:

是三角形商= [

-0.5, -0.5, 0.0,

0.5, -0.5, 0.0,

0.5, 0.5, 0.0,

0.5, 0.5, 0.0,

-0.5, 0.5, 0.0,

-0.5, -0.5, 0.0

];

这些顶点使用普通的顶点缓冲对象 (VBO)发送到着色器,就像我们在清单 1-6 的第一章示例中所做的一样。

在我们的着色器中使用纹理

为了使用纹理,我们需要调整我们的着色器来访问纹理数据。在这个例子中,我们没有为每个顶点使用单独的纹理坐标属性。相反,在我们的顶点着色器中,我们使用位置的 x,y 坐标作为每个顶点的纹理坐标。传入的每个顶点坐标都将在[-0.5,0.5]范围内,所以当我们将它们用作纹理坐标时,我们将两个坐标都加上 0.5 以映射到[0,1]范围。一个可变变量存储纹理坐标并传递给片段着色器,如清单 3-2 所示。

清单 3-2 。 一个基本的顶点着色器,用于计算和传递纹理坐标

四、越来越真实

在这一章中,我们将介绍提高场景真实性的方法。由于适当的照明对我们的视觉感知是如此重要,本章的大部分内容将建立在上一章的基础上,并着重于改进我们的照明模型。具体来说,我们将

  • 讨论平滑阴影和平坦阴影的区别
  • 解释 Phong 照明模型,然后将其实现为一个着色器程序
  • 展示如何添加雾
  • 讨论生成阴影和添加全局照明的技术
  • 混合对象并计算反射和折射

作为一种精神锻炼,注意你当前的环境。如果你在室内,看看你所在的房间。灯光是软的还是硬的?如果你能透过窗户看到太阳,那么阳光与人造光相比如何?哪些物体是闪亮的,哪些是暗淡的?任何物体在其表面反射其他物体吗?确定反射性更强的材料。有透明或半透明的物体吗?

如果你在外面,大气是什么样子的?是清晰还是朦胧?有风吗——物体会被风吹走吗?快速行驶的汽车的影子是什么样子的?你的影子是什么样子的?

问这些类型的问题并深入观察常见的物体和环境,将有助于您了解自然界中发生了哪些类型的复杂交互,并深入了解在我们的渲染中需要模拟和改进哪些内容,以再现逼真的外观。

我们将在本章中努力实现的最终图像如图 4-1 所示。

9781430239963_Fig04-01.jpg

图 4-1 。我们将在本章中构建的最后一个场景

设置

在这一章中,我们将展示一个比漂浮在空中的单个网格更有趣的例子。相反,我们将建立一个有几个球形网格的场景,这些网格在一个代表地面的平面上旋转。为此,我们将首先创建几个可重用的实用程序对象。

在第一章和第三章中,我们使用了 gl-matrix.js 库的矩阵对象和函数。这个库也提供了 vector 对象和函数。

矢量对象

我们将对网格数据执行一些常见的矢量操作。我们在着色器中内置了简单的向量(x,y,z)符号运算,但不是在 JavaScript 中。gl-matrix.js 库使用数字索引,如[0,1,2]:

var n = vec3.create(0.0,1.0,0.0);

console . log(n[1]);//第二个元素

image 更多 gl-matrix.js 的使用示例可以在github.com/toji/gl-matrix/blob/master/README.md在线找到

要使用 x,y,z 分量符号,我们可以使用 Three.js 中包含的全功能向量和矩阵库。虽然我提倡代码重用,但在本章中,我们只需要一些最小的操作,如叉积、长度和规格化函数。在这里我们可以创建一个我们自己的小 vector 对象,如清单 4-1 所示(基于 Three.js 库中的功能)。

清单 4-1。 一个局部矢量对象,只包含我们在本章中需要的功能

//矢量 3.js

向量 3 =函数(x,y,z ) {

this . x = x | | 0;

this . y = y | | 0;

this . z = z | | 0;

};

Vector3.prototype = {

除法:函数{

如果{

this . x/= s;

this . y/= s;

this . z/= s;

}

还这个;

},

交叉:函数(v ) {

var x = this.x,y = this.y,z = this.z

if(向量 3 的 v 实例){

this . x = y * v . z-z * v . y;

this . y = z * v . x-x * v . z;

this . z = x * v . y-y * v . x;

}

还这个;

},

长度:函数(){

返回 math . sqrt(this . x * this . x+this . y * this . y+this . z * this . z);

},

normalize:函数(){

var length = this . length();

返回 this.divide(长度);

},

};

Notice above that we set default values in our constructor of (0,0,0) and also only divide if the passed in value is not 0.

平面类

为了帮助绘制一个平面,在我们的例子中,为了模拟一个其他物体坐在上面的表面,我们添加了一个名为 setupPlaneMesh 的函数(见清单 4-2 )。

清单 4-2。 具有可覆盖属性和索引缓冲区的平面网格

//plane_mesh.js

函数 setupPlaneMesh(n,大小,平移,颜色,纹理)

{

size = (typeof size !== 'undefined') ?大小 : 10.0;

颜色 = (颜色类型 !== '未定义') ?颜色 : [0.5, 0.5, 1.0, 1.0];

translation =(翻译类型!== '未定义')?翻译:[0.0,0.0,0.0];

textured =(纹理类型!== '未定义')?质感:假;

triangle snormals[n]= GL . create buffer();

}

在清单 4-2 中,n 是 vbo 的全局数组的索引。尺寸、平移和颜色参数指的是平面的长度和宽度、初始平移量和颜色。如果没有提供参数,那么我们使用在三元运算中指定的默认值。

要添加网格,我们可以像这样调用:

setupPlaneMesh(3,10.0,[0.0,-1.0,0.0]);

平面设置函数的参数数量是五个,对于更复杂的网格,甚至可以更多。函数签名中的大量参数很难记住,很容易混淆并导致错误。代替清单 4-2 中的代码,我们仍然会设置默认参数,但是会传入一个更加灵活和详细的 JSON 对象来封装我们的数据。假设读者熟悉 JSON。如果你不是,请参考 http://json.org

我们将把清单 4-2 中的代码改为:

函数 setupPlaneMesh(n,选项)

{

options = options | | { };//确保我们有一个 JSON 对象

size =(选项类型. size!== '未定义')?options . size:10.0;

color = (typeof options.color!== '未定义')?options.color : [0.5,0.5,1.0,1.0];

translation =(type of options . translation!== '未定义')?options.translation : [0.0,0.0,0.0];

textured =(选项类型. textured!== '未定义')?options . textured:false;

}

我们现在添加一个新的平面网格,调用如下:

setupPlaneMesh(3,{"translation": [0.0,-1.0,0.0],

【尺寸】:20.0

}

);

在设置了参数顺序的情况下,如果要将“纹理”更改为“真实”,则需要指定其间的任何和所有参数——大小、平移和颜色——即使您使用的是默认值。使用 JSON 对象的第二种方式让我们可以忽略不需要覆盖的参数,也不要求参数按照任何顺序排列。

image 注意本章中的代码并没有针对性能进行优化。因为我们只有几个网格,这无关紧要。然而,对于涉及许多绘制调用的更复杂的场景,我们将需要编写优化的代码。请参考第九章了解最佳实践和提高性能的方法。

球体

为了生成球体网格,函数 setupSphereMesh 如清单 4-3 所示。第一部分让我们设置缓冲指数,半径,平移,颜色,划分,以及是否使用平滑阴影。接下来,我们使用球坐标生成网格。当我们渲染一个球体时,它由水平线(如果地球被建模为球体,请将纬线想象为与赤道平行)和垂直线(请将它们想象为从北极到南极并代表时区)组成。纬线和经线相交的地方就是顶点。顶点向“极点”靠近,而向“赤道”远离细分越多,网格就越接近真实的球体。

image 单位球面上每一点的法线值就是该点本身(缩放或平移前)。记住法向量是垂直指向表面的方向,从原点开始,这个方向就是向量本身。球坐标是单位长度的,所以这个向量已经被归一化了。

清单 4-3。 文件 sphere_mesh.js,生成一个球体网格

函数 setupSphereMesh(n,选项)

{

options = options | | { };//确保我们有一个 JSON 对象

color = (typeof options.color!== '未定义')?options.color : [1.0,0.0,0.0,1.0];

translation =(type of options . translation!== '未定义')?options.translation : [0.0,0.0,0.0];

radius =(选项类型,radius!== '未定义')?options . radius:1.0;

divisions =(type of options . divisions!== '未定义')?选项.划分:30;

smooth _ shading =(options . smooth _ shading!== '未定义')?options . smooth _ shading:true;

textured =(选项类型. textured!== '未定义')?options . textured:false;

//网格生成修改自//http://learning web GL . com/cookbook/index . PHP/How _ to _ draw _ a _ sphere

var latitudeBands =除法,

经度带=划分;

var vertexPositionData = [],

normalData = [],

colorData = [],

textureData = [],

index data =[];

的(订单编号= 0;纬度,经度++)

var theta = latNumber * Math。PI/latitude bands;

var sinet = math . sin(theta):

var cosTheta = math . cos(theta);

for(var long number = 0;longNumber < = longitudeBandslongNumber++) {

var phi = longNumber * 2 * Math。PI/longitude bands;

var sinphi = math . sin(phi);

var cos phi = math . cos(phi);

var x = cosPhi * sinTheta

var y = cosTheta

var z = sinPhi *语法;

var u = 1-(long number/longitude bands);

其中 v =分数字/纬度带;

texture data . push((x+1.0)* . 5);

texture data . push((y+1.0)* . 5);

正常日期。推送(x):

正常日期。push(y);

正常日期。push(z):

color data . push(color[0]);

color data . push(color[1]);

color data . push(color[2]);

color data . push(color[3]);

vertexpositiondata . push(radius * x+translation[0]);

vertexpositiondata . push(radius * y+translation[1]);

vertexpositiondata . push(radius * z+translation[2]);

}

}

的(订单编号= 0;纬度带;经度++) {。

for(var long number = 0;longNumber < longitudeBandslongNumber++) {

var first =(latNumber *(longitude bands+1))+long number;

var second = first+longitude bands+1;

indexData.push(第一);

indexData.push(秒);

index data . push(first+1);

indexData.push(秒);

indexData.push(秒+1);

index data . push(first+1);

}

}

if(!平滑 _ 阴影)

{

//计算平面着色法线

}

triangle snormals[n]= GL . create buffer();

bindBuffer(gl。ARRAY_BUFFER,trianglesNormalBuffers[n]);

gl.bufferData(gl。ARRAY_BUFFER,new Float32Array(normalData),gl。STATIC _ DRAW);

trianglesNormalBuffers[n].itemSize = 3;

triangle snormal buffer[n]. num items = normal data . length/3:

triangle scolor buffer[n]= GL . create buffer();

bindBuffer(gl。ARRAY_BUFFER,triangles color buffers[n]);

gl.bufferData(gl。ARRAY_BUFFER,new Float32Array(colorData),gl。STATIC _ DRAW);

triangle scolor buffer[n]。item size = 4;

三角形缓冲器。numItems = color data . length/4;

triangle dispositicebuffer[n]= GL . create buffer();

bindBuffer(gl。ARRAY_BUFFER,triangles verticebuffers[n]);

gl.bufferData(gl。数组 _ 缓冲区,

新 float 32 array(vertexPositionData),gl。STATIC _ DRAW);

trianglesVerticeBuffers[n].itemSize = 3;

三角形分布缓冲区[n]。num items = vertexposition data . length/3:

中频(纹理)

{

triangle stexcoordbuser[n]= GL . create buffer();

bindBuffer(gl。ARRAY_BUFFER,trianglesTexCoordBuffers[n]);

gl.bufferData(gl。ARRAY_BUFFER,new Float32Array(textureData),

gl。STATIC _ DRAW);

trianglesTexCoordBuffers[n].itemSize = 2;

三角形的。numItems = texture data . length/2;

}

vertxinindexbuffer[n]= GL . create buffer();

bindBuffer(gl。ELEMENT_ARRAY_BUFFER,vertexindex buffers[n]);

gl.bufferData(gl。元素 _ 数组 _ 缓冲区,

新的 Uint16Array(indexData),gl。STREAM _ DRAW);

顶点索引缓冲区[n].itemSize = 3;

vertxinindexbuffer[n]. num items = index ATA . length;

}

我们将在场景中创建一个新的球体,如下所示:

setupSphereMesh(0,{"translation": [-1.0,-0.75,0.0],

【颜色】:【1.0,0.0,0.0,1.0】,

【分工】:20、

“smooth_shading”:假

});

具有 5、10 和 20 细分的网格,如 WebGL 检查器所示(在第九章的中介绍),如图 4-2 中的所示。

9781430239963_Fig04-02.jpg

图 4-2 。纬度和经度分别为 5、10 和 20 的球体

在清单 4-3 中,我们省略了平面阴影代码。在我们讨论了平滑阴影和平坦阴影的区别之后,我们将回到这个代码。

重新审视照明

照明是图形的核心,我们将在本章涵盖更多的灯光实现 细节,从阴影模型、传统的 Phong 照明模型开始,最后是全局辐射模型。

阴影模型

给多边形着色有两种基本方法:平滑着色和平滑着色。平面阴影 表示整个多边形是一种颜色。我们对所有顶点使用相同的法向量。因此,对于同一个顶点,边相交的法线可能不同,这取决于整个面的法线向量值。这种差异意味着相邻边上的照明值会有很大差异,因此您会看到一条边在哪里结束,另一条边在哪里开始。相反,平滑阴影 表示对颜色和法线值进行插值。这可以在顶点着色器(VS)中完成,如 Gouraud 着色,或在片段着色器(FS)中完成,如 Phong 着色。这两种着色技术将在本章后面详细介绍。

法向量再探

让我们先来看看多边形边相交且顶点共享时平面阴影的法向量是什么样子的(见图 4-3 )。

9781430239963_Fig04-03.jpg

图 4-3 。平面着色:一种颜色,每个表面一个法线

正如你在图 4-3 中看到的,共享顶点处的法线是脱节的。相邻多边形的值之间会有明显的跳跃。使用平面着色时,如果入射的镜面反射光没有照射到顶点,镜面反射高光(回想一下镜面反射是在特定方向反射的光)将被忽略。因此,平面着色通常根本不计算镜面反射。

使用平滑着色时,共享顶点与其共享的所有面进行平均。图 4-4 的右边显示了如何使用一个新的法向量,它是两个共享边的平均值。当然,左图也有一些顶点不是跨多个三角形共享的,也有一些是由三个三角形共享的。

9781430239963_Fig04-04.jpg

图 4-4 。平滑着色:平均法线和插值颜色

平滑着色主要有两种类型:Gouraud 着色和 Phong 着色。Gouraud 着色是按顶点执行的,而 Phong 着色是按像素执行的,这样可以更好地捕捉镜面高光。

平面阴影

我们现在将返回 04/sphere_mesh.js 代码,看看我们之前忽略的平面阴影方法。在 WebGL 中,由于 FS 会自动插值结果,因此实际上执行平面着色比平滑着色更困难。对于球体,我们必须改变我们的三角形,使每个顶点都有相同的法线(见清单 4-4 )。

清单 4-4。 计算平面着色法线

if(!平滑 _ 阴影)

{

vertexPositionData = calculatedflattenedvertices(

vertexPositionData,index data);

color data =[];

for(var I = 0;i < indexData.length++i)

{

color data . push(color[0]);

color data . push(color[1]);

color data . push(color[2]);

color data . push(color[3]);

}

normal data = calculatePerFaceNormals(normal data,index data);

}

函数 calculated vertices(origin vertices,indices)

{

var 顶点=[];

for(var I = 0;i < indices.length++i)

{

a =指数[I]* 3;

vertices . push(orig vertices[a]);

vertices . push(orig vertices[a+1]);

vertices . push(orig vertices[a+2]);

}

返回顶点;

}

函数 calculatePerFaceNormals(原始法线,索引)

{

var normal = [];

for(var I = 0;i < indices.lengthi+=3)

{

var a = indexes[I]* 3;

var b =指数[I+1]* 3;

var c =指数[I+2]* 3;

n1 = new Vector3(origNormals[a], origNormals[a+1], origNormals[a+2]);

n2 = new Vector3(origNormals[b], origNormals[b+1], origNormals[b+2]);

n3 = new Vector3(origNormals[c], origNormals[c+1], origNormals[c+2]);

NX =(n1 . x+N2 . x+n3 . x)/3;

ny = (n1.y + n2.y + n3.y)/3;

NZ =(n1 . z+N2 . z+n3 . z)/3;

v3 =新矢量 3(nx,ny,NZ);

normals . push(v3 . x);

normals . push(v3 . y);

normals . push(v3 . z);

normals . push(v3 . x);

normals . push(v3 . y);

normals . push(v3 . z);

normals . push(v3 . x);

normals . push(v3 . y);

normals . push(v3 . z);

}

返回法线;

}

在清单 4-4 中,我们扩展了我们的数据,包括每个索引的颜色、位置和法线数据,而不仅仅是每个顶点。我们使用恒定的颜色,因此扩展颜色数据是微不足道的。对于我们的顶点位置,我们传入原始顶点信息,然后通过查找与每个索引相关的顶点,使用这些信息来产生所有顶点位置(包括重复值)的更长的数组。对于法线,我们取所有三个三角形顶点法线的平均值,并将这个新值用于三角形中的每个顶点。参见图 4-5 。

9781430239963_Fig04-05.jpg

图 4-5 。具有不同细分的球体的平面着色

当我们渲染我们的球体时,我们将使用 drawArrays 方法而不是 drawElements,因为我们不再使用索引缓冲区。我们仍然使用 drawElements 方法来渲染平面:

if(i==3){

gl.drawElements(gl。三角形,顶点索引缓冲区[i]。numItems 冰川。UNSIGNED_SHORT,0);

}否则{

gl.drawArrays(gl。三角形,0,trianglesVerticeBuffers[i]。numItems);

}

平面着色器示例位于文件 04/01_flat.html 中。

朗伯反射

朗伯反射给出了物体任意点的漫射光强度。回想一下,漫射光取决于入射光到曲面点的角度,但是反射是全方位的。计算朗伯反射包括取法线向量 N 和光到表面 L 的方向,然后计算这些向量之间的角度的余弦。角度越大(高达 90 度),余弦值就越低。当角度接近 0 时,余弦接近 1。所有其他角度值将介于-1 和 1 之间,当法线和光照向量垂直时,角度值为 0。(90,270)范围内的角度将返回负值,因为这意味着灯光位于曲面的法线向量的相反侧。

通常负值被箝位为 0。为了计算余弦,我们可以取归一化 N 和归一化 L 的点积,也就是朗伯项 dot(N,L)。光的漫射分量计算如下,其中 M D 和 L D 对应于材料漫射分量和光漫射分量:

漫反射= dot(N,L)*M D *L D

当仅使用漫射颜色和可选的全局环境光因子时,这有时被称为朗伯照明(参见图 4-6 )。

9781430239963_Fig04-06.jpg

图 4-6 。朗伯反射的法线(N)和光照(L)向量

使用 Lambert 照明的 VS 如列表 4-5 所示。

清单 4-5。 计算朗伯量

五、物理学

在这一章中,我们将介绍在我们的场景中物体之间的物理交互建模。我们将在本章中讨论的主题有:

  • 位置、速度、加速度
  • 重力和摩擦力之类的力
  • 抛射体运动
  • 检测碰撞并对其做出反应
  • 弹性和动量守恒
  • 势能和动能

背景

除了照明、纹理和其他现实主义的视觉线索,物体如何与周围环境进行物理交互也可以使我们的动画更加可信。不遵循物理规律的互动看起来很奇怪,也不现实。当然,这可能是我们所追求的效果。然而,在这一章中,我们将集中精力尝试让我们的场景在物理上表现得像我们期望的物体交互一样。

物理模拟的范围是巨大的。我们可以模拟水波或物体的浮力、轮胎的转动、飞机的飞行等等。在这一章中,我们将把范围缩小到基本的运动学:重力,简单碰撞,势能和动能,以及抛射物。

在对场景中的多个移动对象建模时,一个核心要求是能够检测对象何时相互接触。在本章中,我们将建立检测碰撞的方法。

作用在我们身上的力

每一天的每一秒,都有力量在作用于我们。这些力包括重力,它把我们拉向地球;表面法线,支撑着我们;摩擦力,阻止我们不断运动;旋转、风、物体或人推或拉我们的向心力;等等。当这些力的总和相互抵消时,我们就说处于静止状态。

标量和向量

在物理学中,我们处理两类量:标量矢量。标量有大小但没有方向,而矢量有大小和方向。比如速度是标量,质量和时间也是。我们可以说,汽车的速度是每小时 50 英里,这是一个标量。如果我们说汽车以每小时 50 英里的速度向东行驶,那么它就是一个矢量。

变化率

对于物理学的应用,我们通常对向量感兴趣。我们可以测量一个物体的矢量位置或位移,例如沿 x 轴的 20 m。为了计算物体的速度,我们取物体在一段时间内的位移差。换句话说,速度是位移的变化率。加速度是速度的变化率。位移通常用 d 表示,速度用 v 表示,加速度用 A 表示。计算物体在从时间 A 到时间 B、的时间范围内的平均速度的基本公式是:

v =(dB–dA)/(时间B—时间 A

例如,如果 d A = 20m,时间 A = 1s,d B = 30m,时间 B = 5s,则:

v =(30m 20m)/(5s 1s)= 10m/4s = 2.5m/s

类似地,为了计算一段时间间隔内的平均加速度,我们取每个相应时间端点的速度,v A 和 v B :

A =(vBvA)/(时间 B 时间 A

如果 v A = 2.5m/s,时间 A = 1s,v B = 3.0m/s,时间 B = 2s,则:

a =(3.0m/s-2.5m/s)/(2s-1s)= 0.5m/s2

图 5-1 显示了位移随时间变化的样本图,接着是速度随时间变化的曲线图,然后是加速度随时间变化的曲线图。例如,请注意,我们可以在减速时向前移动,也可以在零加速度时快速移动。

9781430239963_Fig05-01.jpg

图 5-1 。左:对象的位置;中心:物体的速度;右图:物体的加速度

我们的第一个代码示例将模拟物体由于重力的影响而自由下落。这里,当我们谈到引力时,我们并不是在模拟所有物体之间的普遍吸引力。这种类型的引力对于在天文学中建立精确的轨道模型是必不可少的,但在我们的日常生活中,虽然存在物体之间的这些引力,例如路上的两个不同的人或汽车,但它们小到可以忽略不计。相反,我们将模拟我们最熟悉的重力类型:从一个物体如球(或人)向下自由落体到地球表面。

代码设置

我们将需要能够以一种更有助于更新的方式来跟踪场景元素,并且通过独立于顶点缓冲对象(VBO )数据来更加灵活。在前面的章节中,我们使用了不相互作用的孤立网格。在这一章中,我们将有物体之间的相互作用,并且需要能够跟踪物理属性并调整它们。为此,我们将创建一个新的球体对象,如清单 5-1 所示。

清单 5-1 。 跟踪球体的物理属性

SphereObject =函数 SphereObject (properties) {

var radius =(properties . radius = = = undefined)?1.0:properties . radius;

var position =(properties . position = = = undefined)?新矢量 3(0.0,0.0,0.0):

属性.位置;

var velocity =(properties . velocity = = = undefined)?新矢量 3(0.0,0.0,0.0):

属性.速度;

var acceleration =(properties . acceleration = = =未定义)?新矢量 3(0.0,0.0,0.0):

属性.加速度;

this.radius = radius

this.position =位置;

this.velocity =速度;

this.acceleration =加速度;

this . vbo _ index = properties . vbo _ index;

}

在清单 5-1 的的球体对象中,我们跟踪球体的半径、位置、速度和加速度。我们还有一个 vbo_index 属性,我们将使用它将每个物理球体对象与相关的 vbo 对象联系起来。

存储信息

我们将在一个数组中存储所有的 SphereObject 元素:

var scene elements =[];

我们将三个球体和平面网格声明为:

setupSphereMesh(0,{

“转换”:[1.0,0.75,0.0],

【颜色】:【1.0,0.0,0.0,1.0】,

}

);

setupSphereMesh(1,{

“翻译”:[0.0,0.0,1.0],

【颜色】:[0.0,1.0,0.0,1.0]

}

);

setupSphereMesh(2,{

“转换”:[1.0,0.25,1.0],

【颜色】:[1.0,1.0,0.0,1.0]

}

);

setupPlaneMesh(3,{"translation": [0.0,1.0,0.0]});

scene elements . push(new sphere object({ " vbo _ index ":0 });

scene elements . push(new sphere object({ " vbo _ index ":1 });

scene elements . push(new sphere object({ " vbo _ index ":2 });

随着本章进展到一个更加通用和灵活的系统,我们将修改这个开始的布局。跟踪元素类似于我们创建第六章中提到的粒子系统,关键区别在于这里的相互作用是确定性的,而粒子系统在本质上是部分未知或随机的。

为了帮助查看场景,我们将展示如何设置一个可通过鼠标点击、拖动和滚动事件进行调整的摄像机。

交互式地调整 摄像机

首先,我们将通过沿 z 轴备份我们的视口来缩小:

mat 4 . identity(mv matrix);

mat4.translate(mvMatrix,[0.0,0.0,–20.0]);

//其他相机变换

我们现在将演示如何捕获鼠标向下、向上和移动事件来调整视图。能够以这种方式改变视图将让我们动态地环视我们的场景。

用鼠标旋转视图

要实现用鼠标移动改变视图,首先我们需要将事件处理程序附加到画布上,如清单 5-2 所示。

清单 5-2 。 捕捉鼠标事件来控制视图

var capture = false,

start = [],

angleX = 0,

角度 Y = 0;

$(文档)。ready(function(){

$("#my-canvas ")。on("鼠标按下",功能(e){

capture = true

start = [e.pageX,e.pageY]:

console . log(" start:"+start);

});

$("#my-canvas ")。on("mouseup ",函数(e){

capture = false

console.log("结束捕获");

});

$("#my-canvas ")。鼠标移动(函数(e) {

如果(捕获)

{

var x = (e.pageX − start[0]);

var y =(e . pagey start[1]);

//更新开始位置

start[0] = e.pageX;

start[1]= e . pagey;

anglex+= x;

angle+= y;

//console . log(" Angle:("+angleX+","+angleY+");

}

});

});

在清单 5-2 的中,mousedown 事件发出一个名为 capture 的布尔标志,该标志应该捕获后续 mousemove 事件以及当前鼠标位置的数据。当 mouseup 事件发生时,我们让标志知道它应该停止捕获数据。当 mousedown 事件开始时,mousemove 事件计算从开始位置的偏移量。然后我们更新起始位置。这很重要;否则,我们将得到非常不稳定的结果。最后,我们增加存储 x 和 y 旋转角度的变量。

然后,在我们的应用中,我们在每一帧上更新我们的多视图矩阵,设置平移量和旋转值:

mat 4 . identity(mv matrix);

mat4.translate(mvMatrix,[0.0,0.0,–20.0]);

mat4.rotate(mvMatrix,angleX2Math)。PI/180.0,[0.0,1.0,0.0]:

mat4.rotate(mvMatrix,angleY2Math)。PI/180.0,[1.0,0.0,0.0]:

image 注意除了将鼠标处理程序附加到画布上,我们还可以将它们附加到整个文档上。这在前面的例子中很有用,因为移出画布将会停止鼠标事件的捕获,并在我们移回画布时产生意想不到的不良结果。鼠标按钮可能仍然是按下的,但是我们需要首先释放它,然后在事件被重新捕获之前再次单击并按住它。

通常最好先进行场景范围的变换,然后进行对象特定的变换。

使用鼠标滚轮控制缩放

滚动鼠标滚轮 常用于控制场景的放大和缩小。为此,我们将为 mousewheel 事件附加一个处理程序:

其中 zoom = 1.0

...

$(文档)。就绪(功能(事件){

$("#my-canvas ")。on("鼠标滚轮",功能(e){

var delta = window . event . wheel delta;

如果(增量> 0)

{

zoom+= 0.1;

}否则{

缩放= 0.1;

//防止负缩放

如果(缩放< 0.01)

{

缩放= 0.1;

}

}

});

...

mat4.scale(mvMatrix,[缩放,缩放,缩放]);

现在,由于浏览器的差异,上面的代码将无法在 Firefox 上使用,因为 Firefox 使用了 DOMMouseScroll 事件而不是 mousewheel 事件。为此,我们可以添加多个事件处理程序:

函数 adjustZoom(增量)

{

如果(增量> 0)

{

zoom+= 0.1;

}否则{

缩放= 0.1;

如果(缩放< 0.01)

{

缩放= 0.1;

}

}

}

$(文档)。就绪(功能(事件){

$("#my-canvas ")。on("鼠标滚轮",功能(e){

adjustZoom(window . event . wheel delta);

}).on(" DOMMouseScroll ),函数(e){

//firefox

adjust zoom(e . original event . detail*-1.0);

});

...

image 注意mouse wheel 和 DOMMouseScroll 事件的目标是鼠标指针当前位置下方的 DOM 元素,类似于 click 事件。

detail 属性的方向与 wheelDelta 相反,因此为了保持一致,我们将其乘以-1。这些属性的大小也不同,但是我们只关心表示向上或向下滚动方向的符号。在来自github . com/brandonaaron/jQuery-mouse wheel/blob/master/jQuery . mouse wheel . js的 jQuery mousewheel 插件中可以找到更健壮的鼠标滚轮事件处理。

本章中所有示例的着色器程序将与 04/05_phong_phong.html 演示中的 Phong 照明模型和着色器相同。我们准备开始模拟物理相互作用,我们要做的第一件事是模拟重力。

重力

正如大多数非物理学家所习惯的那样,重力仅仅是将物体拉向地球的力量。俗话说,“上去的,一定下来。”我们将模拟三个球形球向地面下落,并做一些连续的改进。

自由落体

我们第一次尝试建立重力模型时,会简单地降低每一帧中所有三个球体的位置。对于这个示例,我们将使用 04/05_phong_phong.html 文件中的代码作为起点,并使用前面概述的更改来跟踪场景元素。在清单 5-3 中,我们展示了如何通过搜索合适的 vbo_index 来调整每个球体,以确定哪些对象是球体,然后转换每个球体的模型视图矩阵。

清单 5-3 。 调整选择场景元素

函数 searchForObject(arr,index)

{

for(数组中的变量 I)

{

if(arr[i]。vbo_index == index)

{

影子系统

}

}

return 1;

}

函数 drawScene()

{

for(var I = 0;i < vertexIndexBuffers.length++i)

{

mat 4 . identity(mv matrix);

mat4.translate(mvMatrix,[0.0,–1.0,–15.5]);

var n = searchForObject(场景元素,I);

如果(n!=-1)

{

mat4.translate(mvMatrix,[0 . 0 . 5 . 0-场景元素[n].position.y,0.0]);

scene elements[n]. position . y+= 0.1;

}

mat 4 . tonversemat 3(mvmatrix,标准矩阵);

mat3 .转发器(正常矩阵);

setMatrixUniforms();

...

}

}

在清单 5-3 中,我们有一个助手方法 searchForObject,它接受一个 SphereObjects 的输入数组,并根据输入的 vbo_index 值找到一个合适的对象索引,如果没有找到匹配,则为 1。扩展这种方法将允许我们在场景中潜在地拥有许多不同的对象类型,但是能够只影响匹配特定标准的 VBO 对象——在这种情况下,是一个球体。如果当前的 VBO 索引是匹配的,我们转换它的模型-视图矩阵并增加存储的 y 位置。地面网格的 VBO 指数将导致搜索返回 1,因此它将是固定的。

运行这段代码的结果(可以在 05/01a_gravity.html 文件中找到)是球体无限下落。它们经过地面,如图 5-2 左侧所示。现在让我们添加第一个碰撞检测案例来防止这种情况。

9781430239963_Fig05-02.jpg

图 5-2 。最左边:球体的起始位置;左:不与地面碰撞的自由落体;右:不包括半径的碰撞检测;最右边:正确的碰撞检测

坠落并与地面碰撞

首先,我们将正式确定球体和地面的初始高度:

var INITIAL _ HEIGHT _ TRANSLATION _ OF _ SPHERES = 5.0;

var GROUND _ Y = 1.0;

。。。

setupPlaneMesh(3,{ "translation": [0.0,GROUND_Y,0.0]});

要测试一个物体是否撞到地面,我们需要测试我们球体的起始平移量减去平移的 y 位置是否大于地面高度。如果不是,我们停止增加位置:

。。。

var n = searchForObject(场景元素,I);

如果(n!= −1)

{

if(INITIAL _ HEIGHT _ TRANSLATION _ OF _ sphere-scene elements[n]. position . Y > GROUND _ Y)

{

scene elements[n]. position . y+= 0.1;

}

mat4.translate( mvMatrix,

[0.0,INITIAL _ HEIGHT _ TRANSLATION _ OF _ SPHERES-scene elements[n]. position . y,0.0]);

}

。。。

运行这段代码可以停止球体,但是它们会卡在平面的中间,如图 5-2 右侧的所示。因此,让我们改进我们的碰撞检测,以考虑球体的半径:

if((INITIAL _ HEIGHT _ TRANSLATION _ OF _ SPHERES-

(场景元素[n].position.y +场景元素[n])。半径) ) >地面 _Y)

{

scene elements[n]. position . y+= 0.1;

}

该调整的结果显示在图 5-2 的的最右侧。

让我们再做一个代码改进,直接在我们的 SphereObject 中设置球体的初始平移,而不是在 setupSphereMesh 调用中:

setup spheresh(0,{ "color": [1.0,0.0,0.0,1.0]};

setup spheresh(1,{ "color": [0.0,1.0,0.0,1.0]};

setup spheresh(2,{ "color": [1.0,1.0,0.0,1.0]};

setupPlaneMesh(3,{ "translation": [0.0,GROUND_Y,0.0]});

scene elements . push(new sphere object({ " vbo _ index ":0,

“位置”:新矢量 3(1.0,0.75,0.0)});

scene elements . push(new sphere object({ " vbo _ index ":1,

“位置”:new Vector3(0.0,0.0,1.0)});

scene elements . push(new sphere object({ " vbo _ index ":2,

“位置”:新向量 3(1.0,0.25,1.0)});

这种调整让我们可以在 VBO 代码中保留局部网格坐标和颜色细节,在球体对象中保留世界位置。因为它在 VBO 之外,我们现在可以很容易地调整位置,而无需修改我们的缓冲区。我们现在还需要在 translate 调用中调整我们的 x 和 z 位置:

mat4.translate(mvMatrix,

[场景元素[n].位置. x,

INITIAL _ HEIGHT _ TRANSLATION _ OF _ sphere-scene elements[n]. position . y,

场景元素

]);

我们的下一步是让球体反弹回来。

跌倒了,但又跳起来

让我们在这些球体中放一些弹簧,让它们在飞机撞击时弹回。那么我们如何做到这一点呢?如果地面受到撞击,我们需要改变方向。一种简单的方法是在撞击时翻转位置调整的方向:

函数 isAboveGround(n)

{

return(INITIAL _ HEIGHT _ TRANSLATION _ OF _ SPHERES–

(场景元素[n].position.y +场景元素[n])。半径) >地面 _ Y);

}

。。。

var n = searchForObject(场景元素,I);

如果(n!= −1)

{

if( isAboveGround(n))

{

scene elements[n]. position . y+= 0.1;

}其他{

scene elements[n]. position . y = 0.1;

}

。。。

}

这种方法的问题是球体将开始向上移动,但是因为它在地面上,所以在下一次迭代中它将立即向下移动。会再次到达地面,球体会再次开始向上。它将无限期地这样做,并陷入一个交替循环,使物体轻微晃动,但不会移动太多。

这种方法的另一个补充是添加一个标志,表明地面已被击中,这样一旦地面被击中,我们就永远不会通过继续下落的测试条件:

var flip _ direction =[假,假,假];

。。。

if( isAboveGround(n) &&

!翻转方向[n]

)

{

scene elements[n]. position . y+= 0.1;

}否则{

flip _ direction[n]= true;

scene elements[n]. position . y = 0.1;

}

这确实消除了前面的问题,球在碰撞时会向上弹回。然而,这种方法并不是一个稳健或有用的解决方案,因为一旦球开始向上运动,它将永远向上运动,永远不会再向下运动。

我们现在将看看如何使用速度和加速度来正确地模拟一个弹跳的物体。

落下又弹起;重复

直到现在,我们还没有利用我们的球形物体的速度或加速度属性。

我们可以将等式 A =(vB—vA)/(时间B—时间 A )改写为:

v b = v a + a(时间B—时间 A

或者等效如下,其中 f 代表最终,I 代表初始,t 代表时间间隔:

v f = v i + at

这个方程可以用来模拟我们的自由落体。直到现在,下降的速度始终不变。这是不准确的,因为物体下落时会加速——所以使用这个等式也是对下降真实性的一个改进。当我们把球向上弹回时,在 SphereObject 中存储信息的灵活性就会显现出来。

让我们仔细看看等式 v f = v i + at。时间 t 可以设置为 1,因为我们可以使用帧值偏移来代替实际时间。重力将是加速度 a。通常,重力向下的值为 9.8 米/秒 2 ,但是我们的场景没有使用任何特定的比例或测量单位,所以我们选择的值可以是任何看起来不错的值——太高的值会使下降发生得太快,太低的值会导致下降太慢。价值观的实验是这里的关键。对于我们当前的场景设置,0.01 的加速效果很好。这里显示了一个球体初始化:

sceneElements.push(新的球形对象(

{

“vbo _ index”:0,

“位置”:新矢量 3(1.0,0.75,0.0),

“加速度”:新矢量 3(0.0,0.01,0.0)

}

)

);

正常情况下,加速度表示为负数,但是由于我们平移每个球体的方式,我们使用了正值。如果你想使用负值,你可以调整翻译的符号。

我们每个球体的初始速度向量是(0,0,0),这使得下一帧后的 y 速度:

v fy = v + a 【t = v + 0.01(1) = v + 0.01 = 0.01

所以第一帧之后就是 v fy = 0.01,第二帧之后是 0.02,第三帧之后是 0.03,以此线性类推。当我们将速度应用于我们的距离方程 dfy= diy+vyt 时,这将产生相对于初始位移的位移,在第一帧后为 0.01,第二帧后为 0.03,第三帧后为 0.06,依此类推,非线性增加。

在我们的代码中,我们不是直接增加位置,而是先调整速度,然后再调整位置。这允许我们在与平面接触时逆转速度,而不会陷入循环:

if( isAboveGround(n))

{

场景元素[n].速度. y + =场景元素[n].加速度. y;

}否则{

场景元素[n]. velocity . y * = 1.0;

}

scene elements[n]. position . y+= scene elements[n]. velocity . y;

当你运行程序 05/01e_gravity.html 时,三个球会继续无限地上下弹跳。

非完美弹性

当前面例子中的球反弹时,它们的弹性非常好。完美弹性意味着它们在碰撞后向上运动的速度与它们在那一刻下落的速度相同。没有动量因摩擦或其他形式的能量而损失。除了在理论上,物体不是完全弹性的,所以我们可以通过降低弹性来使例子更真实。这意味着反弹将在某一点停止。这非常容易建模;我们只是把弹性作为一个变量加进去,然后乘以与地面发生碰撞时的速度:

var 弹性= 0.8;

。。。

if( isAboveGround(n))

{

场景元素[n].速度. y + =场景元素[n].加速度. y;

scene elements[n]. position . y+= scene elements[n]. velocity . y;

}否则{

//首先减去速度,这有助于防止卡住

scene elements[n]. position . y = scene elements[n].

sceneElements[n].velocity.y *=弹性;

}

弹性值的范围可以从 0.0 到没有弹性(停止不动;想象撞上一堵砖墙)到 1.0 表示完全弹性(有史以来最伟大的橡胶球,只是理论上可能)。在前面的代码中,我们还确保在弹性系数乘以速度之前调整位置。这有助于防止球突然停止。

为了说明这一点的必要性,请考虑一个距离地面 0.6 米、当前速度为 1.0 的物体。当速度方向切换到 1.0 时,它应该能够在下一次迭代中回到上面。但是,如果对象的弹性值为 0.4,这会将返回速度抑制为 0.4,而不是 1.0。下一个计算的位置将是 0.2,这意味着它仍然在表面下 0.2。这意味着下一次通过地面测试时,它会再次失败,速度会翻转。这是一个坏消息,因为物体在地面下,并再次向下移动。力度被反转并再次衰减到 0.16,这使力度降低到地面的 0.36,然后翻转到力度 0.064,依此类推。这种情况的结果是,在几次迭代之后,一个本来可以快速行进的物体可能会看起来像死了一样停在它的轨道上。至少可以说,这看起来很奇怪。在弹性倍增之前将速度添加到位置可以消除这个问题。弹跳球的最终版本如图图 5-3 所示。

9781430239963_Fig05-03.jpg

图 5-3 。弹跳球

在下一个例子中,我们将释放任意数量的球体到世界中,并有初始的 x,y 和 z 速度。

三维速度

我们将检查是否碰到了我们飞机的无形边界,如果超出了,就把球弹回到我们的区域内。一旦我们设置好了,我们还将测试球体之间的碰撞 。

检测与许多墙壁的碰撞

我们将在场景中添加任意数量的球体。首先,我们将添加一些代码来防止具有 x 和 z 速度的物体离开我们的观察区域。我们将测试与地面网格的虚拟墙壁的交集:

if(scene elements[n]. position . x > PLANE _ SIZE | | scene elements[n]. position . x < PLANE _ SIZE)

{

scene elements[n]. position . x+=(1.0 * scene elements[n].

场景元素[n]. velocity . x * = 1.0;

}否则{

scene elements[n]. position . x+= scene elements[n]. velocity . x;

}

if(scene elements[n]. position . z > PLANE _ SIZE | | scene elements[n]. position . z < PLANE _ SIZE)

{

scene elements[n]. position . z+=(1.0 * scene elements[n].

场景元素[n]. velocity . z * = 1.0;

}否则{

scene elements[n]. position . z+= scene elements[n]. velocity . z;

}

到目前为止,我们已经检测到与移动物体和不可移动物体的碰撞。现在我们将模拟运动物体相互碰撞。

相互碰撞

当我们想知道两个物体是否发生碰撞时,使用明确定义的形状来测试包围盒更简单。这将大大降低计算成本。存在其他体积,如椭球体和圆柱体,但最常用的是长方体和球体。

边界框和球体

我们选择的包围体取决于底层对象的形状。相当圆的物体用球体自然表现得很好,而许多其他物体更适合方形盒子。图 5-4 显示立方体不太适合球体,反之亦然。(这两件事我们都不想做。)

9781430239963_Fig05-04.jpg

图 5-4 。左图:包围球中的立方体;右图:边界框中的球体

image 关于球体和立方体几何图形的复习,请参考en.wikipedia.org/wiki/Sphereen.wikipedia.org/wiki/Cube

有了包围体,我们可以使用简单的几何图形来处理彼此靠近的对象。例如,我们知道,如果两个边界球的中心之间的距离小于它们半径之和,则这两个球相交。有了包围盒,我们就可以保证在不知道的情况下不会发生碰撞。如果包围体精确地表示对象,碰撞总是准确的。然而,如果包围体大于被保持的物体,当我们认为发生了碰撞(但实际上没有)时,就会有一些误报。被封装的对象离它的包围体越近,我们遇到的相交的假阳性就越少。

限制这种误差的一种方法是将不规则形状的网格分成更小的边界框或球体。随着较小的包围体(或 2D 情况下的区域)的数量增加,误差量减少并且将接近零。图 5-5 显示了 2D 不规则形状和边界矩形,以及几个更接近形状的边界矩形,但也增加了我们必须执行的计算检查的数量。包围矩形内的空白区域显示了误报的碰撞区域。

9781430239963_Fig05-05.jpg

图 5-5 。左:单个边界框;右图:四个边界矩形

现在我们准备探测球体之间的碰撞。首先,为了现实地处理碰撞,我们需要知道一些关于动量和动量守恒的知识。

动量守恒

动量 p 是物体质量 m 和速度 v 的乘积:

p =多视图

当两个物体碰撞时,理论上系统的整体动量保持不变。在现实中,摩擦也会发生,所以没有碰撞是完全弹性的。

动量守恒方程如下:

p1 _ 初始+p2 _ 初始= p1 _ 最终+p2 _ 最终

它可以重写为:

m1v1i2+m2v2i2= m1v1f2+m2v2f??

当你求解 v 1f 或 v 2f 时,你会得到这个:

v1f=[(m1—m2)/(m1+m2)]v1i+【2m2/(m1+m2)v2i

类似地:

v2f=[(m2m1)/(m1+m2)]v2i+【2m1/(m1+m2)v1i

当 m 1 = m 2 的质量时,第一个方程简化为:

v1f= 0/2m2* v1i+2m2/2m2* v2i= 0 * v1i+1 * v2i= v2i

同样:

v 2f = v 1i

所以速度被简单地交换了!

该方程特定于一个维度,但它也适用于正交(垂直)分量,因此我们可以将该方程分别应用于 x、y 和 z 三个维度。

均匀质量碰撞

如前所述,当质量和台球一样完全相同时,我们可以交换速度。

对于每一帧,我们将检查场景中的所有球体是否与场景中的其他物体发生碰撞(见清单 5-4 )。

清单 5-4 。 检查质量相等的球体之间的碰撞

冲突检查(sceneElements,n);

。。。

冲突的函数检查(arr,n)

{

for(数组中的变量 I)

{

如果(我!= n)

{

var p1 = arr[n]。位置;

其中 p2 = arr[i]的位置;

var v =新向量 3(P1 . x-p2 . x、P1 . y-p2 . y、P1 . z-p2 . z);

if(v.length() < (2.0 * arr[n]。半径) )

{

//交换两个向量的速度

var tmp = arr[n]。速度;

安排,安排。速度= arr[i]。速度;

arr[i]。速度= tmp

//移动位置,这样就不会卡住

arr[n]. position . x+= arr[n]. velocity . x;

arr[n]. position . y+= arr[n]. velocity . y;

arr[n]. position . z+= arr[n]. velocity . z;

arr[I]. position . x+= arr[I]. velocity . x;

arr[I]. position . y+= arr[I]. velocity . y;

arr[I]. position . z+= arr[I]. velocity . z;

}

}

}

}

在清单 5-4 中,我们检查距离是否小于半径的两倍,因为半径是相同的。如果发生碰撞,我们用一个临时变量来交换速度。其结果如图 5-6 左侧所示。

9781430239963_Fig05-06.jpg

图 5-6 。左图:均匀质量的碰撞;右图:不同质量的碰撞

我们现在将创建不同半径和质量的球体,并计算它们之间的碰撞。

不同质量的碰撞

在下一个例子中,我们将使用不同半径的球体。我们将假设所有球体的材料都是相同的,并且它们是实心的。这让我们可以使用体积来按比例比较质量,而无需实际设置或知道任何球体的质量。回想一下,一个球体的体积是 V = 4/3πr 3

假设我们有两个球体:V1= 4/3 π r13和 V2= 4/3 π r23。这两个体积的比值为 V1/V2=(r1/r2)3

如果 r 1 = 1,r 2 = 1,则比值= 1 3 = 1。体积和质量(因为它们是相同的材料)也是相同的。若 r 1 = 2,r 2 = 1,则比值= (2/1) 3 = 8。所以第一个球的体积是第二个球的八倍,质量也是第二个球的八倍。我们可以一般地使用两个球体的半径来设置我们的两个质量如下:

m1/m2=(r1/r2)3/1

m1=(r1/r2)3

m 2 = 1

image 注意对于更复杂的计算,我们可以使用现有的物理库,比如在第八章的中讨论的那些。

给定初始速度和半径,我们可以计算每个球体的最终速度值,如清单 5-5 所示。

清单 5-5 。 检查不同质量球体间的碰撞

冲突的函数检查(arr,n)

{

for(数组中的变量 I)

{

如果(我!= n)

{

var p1 = arr[n]。位置;

其中 p2 = arr[i]的位置;

var v =新向量 3(P1 . x-p2 . x、P1 . y-p2 . y、P1 . z-p2 . z);

if(v.length() < (arr[i].半径+数组[n]。半径) )

{

//交换两个向量的速度

var tmp1 = arr[n]。速度;

var tmp2 = arr[i]。速度;

var r1 = arr[n].radius;

var r2 = scar[i].半径;

var finalX = findfinalvides(tmp 1 . x,tmp2.x,r1,R2);

var finally = findfinalvides(tmp 1 . y,tmp2.y,r1,R2);

var finalZ = findfinalvides(tmp 1 . z,tmp2.z,r1,R2);

安排,安排。velocity = new Vector3( finalX[0],finalY[0],finalZ[0]);

arr[i]。velocity = new Vector3( finalX[1],finalY[1],finalZ[1]);

//移动位置,这样就不会卡住

arr[n]. position . x+= arr[n]. velocity . x;

arr[n]. position . y+= arr[n]. velocity . y;

arr[n]. position . z+= arr[n]. velocity . z;

arr[I]. position . x+= arr[I]. velocity . x;

arr[I]. position . y+= arr[I]. velocity . y;

arr[I]. position . z+= arr[I]. velocity . z;

}

}

}

}

函数 findFinalVelocities,v2,r1,r2)

{

var m1 = (r1r1r1)/(r2r2r2);

凡 m2 = 1.0

var f1 =(m1-m2)/(m1+m2)* v1+2 * m2/(m1+m2)* v2;

var F2 =(m2-m1)/(m2+m1)* v2+2 * m1/(m2+m1)* v1;

return [f1,F2];

}

在清单 5-5 的中,我们添加了一个助手方法 findFinalVelocities,它接受两个初始速度和半径,计算并返回最终速度值。我们按组件进行计算。在图 5-6 的右边显示了不同大小的球体相互作用。

我们的下一个例子着眼于抛射体的路径。

射弹

我们都熟悉物体的抛射运动,无论是发射的炮弹、弓箭手的箭、被击中或投掷的棒球等等。抛射体有一个物体行进的抛物线弧,如图图 5-7 所示。

9781430239963_Fig05-07.jpg

图 5-7 。典型的投射路径

除非有风或其他水平力,否则水平速度分量 v x 在物体的整个飞行过程中保持不变。由于重力的作用,垂直速度随时间而减小,计算为(v y + a y t)。

image 给定一个初始速度矢量 ,v,一旦我们计算出初始正交的 x 和 y 速度分量,v x 和 v y ,我们就可以使用这些等式分别计算未来的速度:

vFX= v【IX】+a【x】t 和 v【fy】= v【iy】+a

影响抛射体飞行的基本因素有两个 (我相信玩过《愤怒的小鸟》的人都很熟悉):初速的角度和大小。45 度角的初始水平和垂直速度相等。在 0 到 90 度之间,任何大于 45 度的角度都将具有更大的垂直速度,而任何小于 45 度的角度都将具有更大的水平速度。给定速度矢量和地面之间的角度θ,初始垂直分量 v y 是 sin(θ),而初始水平分量 v x 是 cos(θ)。

假设我们的初速度是 25 m/s,角度是 60 度。那么 v y = 21.65m/s,v x = 12.5m/s .在平坦的表面上,当 y 分量距离方程 的位移为 0 时,具有这个初速度的物体会撞击地面:

d = v t + 1/2a y *t 2

这通过解决以下问题来实现:

0 = t(v+1/2 * ay * t)

第一个解通常出现在 t = 0s 时。第二种解决方案出现在以下情况:

t = 2viy/ay

= 2(21.65 米/秒)/(9.8 米/秒 2

= 4.42 秒

根据我们刚刚计算的停留时间,我们可以确定物体将移动的垂直距离如下:

d = vXi* t+1/2 * ax* t2

= 12.5 米/秒* 4.42 秒+ 0

= 55.25 米

image 求抛体的最大高度,可以取初速度 y,V iy ,解方程 Vfy2= Viy2+2ad 为 V fy = 0 时。这将对应于抛射体路径的顶点,在此处抛射体开始向下返回:d = Viy2/2a = Viy2/19.6 米/秒 2

我们现在将实现一个发射炮弹的演示。这个演示的主要新组件是听击键来调整一个半开盒子网格的角度,这个半开盒子网格代表我们的初速度的角度和初速度的速度。我们也听一个钥匙从这个盒子里发射一个球体。这里显示了 快捷键:

$(文档)。keyup(函数(evt){

交换机(evt.keyCode)>

案例 80: //'p '

暂停了=!暂停;

打破;

案例 83: //'s '

-角度;

打破;

案例 68: //'d '

++角度;

打破;

案例 37: //'左'

速度= 0.1;

打破;

案例 40: //'右'

速度+= 0.1;

打破;

案例 70:

火=真;

console.log("开火!");

场景元素[0]。位置=新向量 3(0.0,0.0,0.0);

场景元素[0]。速度=新矢量 3(

speedMath.cos(角度.1),speedMath.sin(角度.1),0.0);

打破;

默认值:

打破;

}

});

fire 事件重置球体的位置,然后根据角度设置速度。当我们对场景进行变换时,平移、旋转和缩放的顺序很重要。我们在这里执行的 gl-matrix.js 库中的一个新方法是缩小我们的场景,以便更容易看到抛射体的路径:

var 标度= 0.2;

。。。

mat4.scale(mvMatrix,[scale,SCALE,SCALE]);

当 f 键被按下并且火标志被设置时,我们更新我们的球体位置:

如果(火){

scene elements[0]. velocity . y+= scene elements[0].加速度. y;

scene elements[0]. position . x+= scene elements[0]. velocity . x;

scene elements[0]. position . y+= scene elements[0]. velocity . y;

scene elements[0]. position . z+= scene elements[0]. velocity . z;

}

为了在不清除浏览器的情况下看到投射体的完整路径,我们可以告诉 WebGL 在初始化时保留绘图缓冲区,并在帧之间调用 gl.clear:

GL = canvas . get context(" webgl " { preserve drawing buffer:true })| |)

canvas . get context(" experimental-web GL ",{ preserveDrawingBuffer:true });

图 5-8 右侧显示了显示完整抛射体路径的输出。

9781430239963_Fig05-08.jpg

图 5-8 。左图:飞行中的弹丸;右图:未清除绘图上下文的射弹

这个演示的完整代码在 05/05 _ propeline . html 文件中。我鼓励你继续玩抛射体和动量游戏。例如,利用这里获得的知识,您可以编写一个简化版的网球程序。

本章的最后一个例子研究了势能和动能之间的关系。

势能

到目前为止,我们一直在看有动能的例子,动能就是运动的能量。另一方面,势能是储存的能量,通常是因为物体的高度和物体自由下落时重力施加的力。势能的一个经典例子是过山车。在过山车的顶部,当汽车静止时,系统中的能量是纯势能(PE)。当每辆车开始下降时,PE 被转换成动能(KE) ,飞车获得速度。当汽车到达地面时,KE 的比值增加,当汽车向上返回时,比值减小。

理论上系统总能量保持不变,如图图 5-9 所示。然而,在现实世界中,能量会因为摩擦力而损失。

9781430239963_Fig05-09.jpg

图 5-9 。没有摩擦,系统的 PE 和 KE 是守恒的

在我们的下一个例子中,我们将创建一个带有跳跃的小斜坡,并让一个球体沿着它移动。我们可以调整高度来确定一个值,这个值将产生足够的速度使它通过。图 5-10 显示了 2D 计划的坡道轮廓。实际的坡道将是三维的 ,但是像这样的图表对于规划网格是有用的。

9781430239963_Fig05-10.jpg

图 5-10 。左:坡道侧视图;中心:剖成三角形的侧视图;右:侧视图,边缘将测试与球的碰撞

建模的第一步是定义一些变量,以便我们可以轻松调整尺寸:

//斜坡尺寸

变化高度 _1 = 65.0,

HEIGHT_2 = 15.0,

HEIGHT_3 = 20.0,

HEIGHT_4 = 15.0,

长度= 60.0,

长度 _2 = 60.0 * 0.5,

长度 _3 = 60.0 * 0.75,

LANDING_RAMP_START =长度* 2.0,

LANDING_RAMP_END =长度* 3.0,

深度= 25.0;

先前的高度对应于初始最大高度、坡道平坦部分的高度以及跳跃/间隙之前的最后一个峰值和着陆坡道的初始高度。图 5-10 中的图不是这个比例;这是一个可以调整到任何尺寸的准则。长度决定了第一个渐变到间隙的距离,比例用于将模型缩放到更适合场景其余部分的大小。

我们将使渐变都是一种颜色,一种不需要每个顶点的颜色数据的方法是禁用网格的属性数组,并指定一个单独的向量:

GL . disablevertexattribarray(vertxcolrattray 属性);

GL . vertixriib 4f(vertixolrattray 属性,1.0,0.9,0.7,1.0);

网格的完整顶点和索引值 在 05/06_ramp.html 文件中,法线按照本书前四章的程序生成。斜坡网格和球体位置,以及完整路径的视图,在图 5-11 中显示。

9781430239963_Fig05-11.jpg

图 5-11 。左图:渲染的斜坡和移动的球体;中心:查看球体的完整路径;右图:路径的另一个视图

剩下的工作是计算与图 5-10 右侧所示的四个边缘的碰撞,并计算与每个边缘角度的余弦和正弦值相关的速度分量。为了实现这一点,我们将首先创建一个新对象来代表球体可能遇到的 2D 墙,如清单 5-6 所示。

清单 5-6 。 存储墙壁属性的一个对象

WallObject =函数 WallObject(属性){

var start _ x =(properties . start _ x = = = undefined)?0.0:properties . start _ x;

var start _ y =(properties . start _ y = = = undefined)?0.0:properties . start _ y;

var end _ x =(properties . end _ x = = = undefined)?0.0:properties . end _ x;

var end _ y =(properties . end _ y = = = undefined)?0.0:properties . end _ y;

this.slope = 0.0

if((end _ x start _ x)> 0.0001 | |(end _ x start _ x)

this . slope =(end _ y start _ y)/(end _ x start _ x);

}

this.start _ x = start _ x

this.start _ y = start _ y

this.end _ x = end _ x

this.end _ y = end _ y

var a =[start _ x end _ x,start _ y end _ y];

这个角度= 0.0;

this.angle = Math.atan2( a[1],a[0]);

}

我们跟踪每条墙线的两个端点:坡度和角度。我们将所有四个墙表示添加到一个名为 ramp_walls 的数组中。这个结构中的每个插入看起来像这样:

p 在哪里

“start _ x”:0.0,

" start_y": HEIGHT_1,

“end_x”:长度 _2,

" end_y ":高度 _2

};

ramp _ walls . push(new wall object(p));

在每个动画帧上,检查 与每面墙的碰撞,跟踪我们球体的总速度,并计算 x 和 y 的速度和位置,如列表 5-7 所示。

清单 5-7 。 检查墙壁碰撞并计算总速度和分量速度及位置

冲突的函数检查()

{

var x = sphere . position . x/SCALE;

var y = sphere . position . y/SCALE;

if(sphere . position . y < 0.0){ return;}//检查接地

var found = false

for(斜坡墙中的变量 I)

{

if( x > = ramp_walls[i]。start_x && x < = ramp_walls[i]。end_x)

{

发现=真;

if(ramp_walls[i].斜率< −0.001 || ramp_walls[i].slope > 0.001)

{

if(ramp_walls[i].斜率> 0.001)

{

sphere . total _ velocity = sphere . acceleration . y;

}否则{

sphere . total _ velocity+= sphere . acceleration . y;

}

//console . log(sphere . total _ velocity);

sphere . velocity . x = sphere . total _ velocity * math . cos(ramp _ walls[I])。角度);

sphere . velocity . y = sphere . total _ velocity * math . sin(ramp _ walls[I])。角度);

sphere . position . y+= sphere . velocity . y;

}

sphere . position . x+= sphere . velocity . x;

}

}

if(!找到){

sphere . velocity . y+= sphere . acceleration . y;

sphere . position . x+= sphere . velocity . x;

sphere . position . y+= sphere . velocity . y;

}

}

在前面的代码中,如果我们不在有围墙的区域,则 found 为 false,并且我们对自由落体进行建模。如果我们在一个有墙的部分,我们检查斜率并适当地加上总速度。零斜率导致纯水平运动,没有加速度(因为我们忽略了摩擦力)。我们用壁角的正弦和余弦乘以总速度来计算 x 和 y 分量的速度。从图 5-11 中,你可以看到路径很接近,但并不完全精确。更高的速度 将显示自由落体和在着陆坡道上的位置之间更突然的变化。提高精确度的一种方法是在退出自由落体之前检查球体是否与墙壁相交。此函数返回的数字的符号表示一个点在线的哪一侧(零在线):

函数 getSideOfWall(wall,x,y)

{

其中 delta = 0.00001

var v =(wall . end _ x wall . start _ x)*(y wall . start _ y)–

(wall . end _ y wall . start _ y)*(x wall . start _ x);

if(v

return 1;

} else if(v)>(0.0+差值)}

返回 1;

}

返回 0;

}

实现这种检查的任务留给了读者。如果你雄心勃勃,想要建立一个过山车模型,那么你的计算中需要考虑更多的因素,比如向心力。

摘要

本章介绍了物体的一些物理属性,并模拟了重力、碰撞和抛射。在下一章,我们将讨论分形、高度图和粒子系统的数学主题。在第八章中,当我们介绍一些可以执行复杂得多的计算的物理库时,我们将回到物理。

六、分形,高度图,粒子系统

这一章介绍了我们可以用数学达到的各种效果。我们将涉及的主题包括:

  • 直接用 GPU 绘画
  • 分形和 Mandelbrot 集简介
  • 高度图和地形生成
  • 用鼠标旋转摄像机
  • 粒子系统

因为我长久以来一直迷恋于数学和它所产生的美丽意象的强烈交集,所以这一章对我来说写起来特别有趣。即使你不是特别喜欢数学,你也可以跳过大部分细节/技术解释,直接用代码进行实验。我确信这里展示的例子和技术会让你感兴趣,并且可以被修改用于任何 WebGL 程序。

用 GPU 直接绘画

在讨论分形图像和 Mandelbrot 集合之前,我们将展示如何用纯粹包含在着色器程序中的逻辑来绘制图像。我们的 WebGL 应用将使用的唯一几何图形是来自五个顶点的四个三角形,它们将形成一个平面。然后,我们将使用图形处理单元(GPU) 的片段着色器,以编程方式设置平面上每个单独的像素颜色。不会对视图进行任何操作。方形平面的设置如清单 6-1 所示。

清单 6-1 。功能在 xy 平面上创建一个由两个三角形组成的正方形

函数 createSquare(size){

size = (typeof size !== 'undefined') ?大小 : 2.0;

var vertexPositionData = [

0.0, 0.0, 0.0,

-尺寸/2.0,-尺寸/2.0,0.0,

尺寸/2.0,-尺寸/2.0,0.0,

尺码/2.0,尺码/2.0,0.0,

-尺寸/2.0,尺寸/2.0,0.0,

];

凡指数数据= [0,1,2,2,3,3,4,4,1];

triangle deployment buffer = GL . create buffer();

gl.bindBuffer(gl)。ARRAY_BUFFER,三角形分布缓冲区:

gl.bufferData(gl。ARRAY_BUFFER,new float 32 ARRAY(vertexPositionData),

gl。STATIC _ DRAW);

三角形分布缓冲区。item size = 3;

triangle deployment buffer . num items = vertexposition data . length/3:

vertxinindexbuffer = GL . create buffer();

bindBuffer(gl。ELEMENT_ARRAY_BUFFER,vertexindex BUFFER);

gl.bufferData(gl。ELEMENT_ARRAY_BUFFER,new Uint16Array(indexData),gl。STREAM _ DRAW);

vertexIndexBuffer.itemSize = 3;

vertxinindexbuffer . num items = index ATA . length:

}

清单 6-1 中平面的默认尺寸是 2.0 x 2.0。

我们的顶点着色器获取 x,y 输入坐标,并将它们传递给片段着色器。z 值固定为 0.0。

七、Three.js 框架

有许多 WebGL 框架可用于抽象出我们在本书前六章中提到的低级应用编程接口(API)调用。这种抽象有助于使 WebGL 开发更加容易和高效。我们将在下一章讨论几个 WebGL 框架。在这一章中,我们将集中讨论最广泛使用的框架之一——three . js。我们将涵盖以下内容:

  • 图书馆的背景
  • 如何用 Three.js 开始开发
  • 如果不支持 WebGL,则退回到 2D 画布上下文进行渲染
  • Three.js API 调用来轻松创建相机、对象和使用光照模型
  • 展示与前面章节中的一些例子等价的 Three.js 代码,这些例子使用了直接的低级 WebGL API 调用
  • 介绍 tQuery,这是一个将 Three.js 与 jQuery 选择器结合在一起的库

背景

Three.js 由 Ricardo Cabello(又名 Doob 先生)创建,自 2010 年以来一直在 gitHub 上。从那时起,它得到了许多贡献者的额外帮助,它的用户群已经发展到一个很大的规模。

Three.js 提供了几种不同的绘制模式,如果不支持 WebGL,可以退回到 2D 渲染上下文。Three.js 是一个设计良好的库,使用起来相当直观。默认设置减少了所需的初始工作或“样板文件”的数量。设置可以作为对象构造时传入的参数来重写,也可以通过随后调用适当的对象方法来重写。

image 注意从 WebGL 开始的人可能会有一个错误的观念认为 Three.js 和 WebGL 开发是一回事。就像 JavaScript 框架 jQuery 和 JavaScript 不一样,Three.js(或者其他任何框架)和纯 WebGL 开发也不一样。

如果你精通一门底层语言,你通常可以理解它的框架代码。反之则不然。了解一个框架并不能保证你了解一门语言,所以学习低级语言是非常有益的。

特征

以下是 Three.js 框架众多特性中的一些:

  • 当不支持 WebGL 时,优雅地退回到 2D 上下文
  • 内置向量和矩阵运算符
  • 摄影机、灯光、材质、着色器、对象和常见几何体的 API 包装实现
  • 导入和导出实用程序
  • 良好的文档和示例

设置

我们现在将讨论如何获得 Three.js 库代码、它的目录结构和核心对象。

获得库

three . js 项目在 https://github.com/mrdoob/three.jsgithub 上托管。最新版本可以从github.com/mrdoob/three.js/downloads下载。或者,如果您熟悉 git,您可以克隆存储库:

git clonehttps://github . com/mrdoob/3 . js . git。

该库正在积极开发中,对 API 的更改并不少见。最新的完整 API 文档可以在 URL mrdoob.github.com/three.js/docs/latest/,找到,它将重定向到当前版本。在github.com/mrdoob/three.js/wiki/有一个 wiki 页面,网上也不乏使用 Three.js 的演示或关于 Three.js 开发的文章。一些较好的文章列在附录 D 中。

目录结构

一旦您下载或克隆了存储库,您就可以将文件放在您的活动开发文件夹中。目录结构显示如下文件夹布局:

/生成源文件的压缩版本

/docs API 文档

/示例示例

/导出 Three.js 源代码的阿桂拖放式 gui 生成器

/src 源代码,包括中央的 Three.js 文件

/utils 实用程序脚本,如导出程序

在 src 目录中,组件被很好地分成以下子文件夹:

/src

/相机相机对象

/core 核心功能,如颜色、顶点、面、矢量、矩阵、数学定义等

/额外的实用程序、助手方法、内置效果、功能和插件

/灯光灯光对象

/材质网格和粒子材质对象,如 Lambert 和 Phong

/objects 物理对象

/renderers 呈现模式对象

/scenes 场景图形对象和雾函数

/textures 纹理对象

Three.js 中心文件

基本要素

Three.js 中有几个核心对象类型(见表 7-1 )。

表 7-1 。Three.js 中的核心对象

基础对象 描述
三个。渲染器 实际渲染场景的对象。实现可以是 CanvasRenderer、DOMRenderer、SVGRenderer 或 WebGLRenderer。
三个。事件 存储场景中包含的对象和灯光的场景图。
三个。照相机 虚拟相机;可以是透视照相机或正投影照相机。
三个。对象 3D 许多对象类型,包括网格,线,粒子,骨骼和精灵。
三个。光 轻型模型。类型可以是环境光、方向光、点光或聚光灯

关于对象层次的另外两个注意事项:三。网格对象有一个相关的三个。几何和三。物质对象,依次各三个。几何包含三个。顶点和三。面部物体。

基本用法

现在我们已经获得了 Three.js 库,我们准备开始使用它。我们需要包含来自本地资源的脚本,如下所示:

或者从 github 远程访问,例如:

你好世界 !

使用 Three.js 与我们到目前为止所做的底层编码相比是非常容易的。虽然已经学习了基本的 WebGL API 调用,但是我们可以在了解(或者至少在不实际检查库代码的情况下假设了解)surface Three.js API 调用下面发生了什么的同时,充分体会到框架的加速。

在我们的第一个例子中,如图 7-1 所示,我们将用 Three.js 渲染一个没有照明的长方体。

9781430239963_Fig07-01.jpg

图 7-1 。用 Three.js 渲染的矩形长方体。没有光线会使长方体显得扁平

与纯 WebGL 相比,该示例的完整代码相当短(参见清单 7-1 )。我们将在清单之后详细讨论代码的每一部分。

清单 7-1。 渲染一个未点亮的长方形长方体

<title>Three.js 立方体测试</title>

八、生产力工具

在前一章中,我们看到了优秀的 Three.js WebGL 框架,并展示了它如何抽象低级 WebGL API 调用。这种抽象简化并加速了开发。这一章介绍了一些额外的工具,可以帮助你的开发更有效率和更愉快。我们将在本章中讨论的主题如下:

  • 使用 WebGL 框架的优点和学习核心 WebGL 的好处
  • 目前可用的框架
  • 基本的“Hello World”philoGL 和 GLGE 框架示例
  • 加载现有网格和模型
  • 文件格式和导入/导出工具
  • 查找和修改现有着色器和纹理
  • JavaScript 物理框架
  • 使用 Three.js 的 physi.js 库的物理演示

框架

一个框架 抽象了底层 API 调用,也扩展了内置功能。WebGL 框架让您开始使用较少的初始设置和样板代码。这使得开始编程更快,开发复杂的应用更容易。框架可以抽象顶点缓冲对象(VBO)和着色器处理,简化相机操作,执行矩阵数学运算,加载网格等。

使用框架的代价是对底层细节的抽象可能会限制可配置性和性能(不需要修改框架源代码)。不过,通常节省的时间超过了可配置性的损失。

然而,不要依附于任何一个框架,先学习基本的 WebGL API 是有益的。这样做的基本原理是,了解核心 WebGL 的工作方式应该能够相当容易地在框架之间切换。相反不成立。如果你首先学习一个框架(不管它有多好),然后需要使用基本的 WebGL API 或切换框架,你可能会很迷茫。此外,如果您了解核心 WebGL 并希望了解特定框架的细微差别,您可以查看源代码并了解基本的 WebGL API 调用。

许多选择

WebGL 有很多很多可用的框架。根据 http://www.khronos.org/webgl/wiki/User_Contributions 的说法,目前有 25 个可用的框架。你应该使用哪一个?

为了脱颖而出,构建了几个框架来提供特定的利基用途,如一般用途、游戏开发、数据可视化、地球仪和地图以及高性能。除了关注点不同,还有哪些因素决定了使用哪个框架?和其他软件项目一样,这里有一些评估标准:

  • 力量和功能 :它可以工作,让你快速创建使用高级技术的场景。代码写得很好,可扩展,很少崩溃,也没有很多严重的错误。
  • 可用性 :清晰的 API 和好的文档,wiki,FAQ 等等。
  • 支持和活动 :作者和社区积极参与。Bug 修复相当快,新的特性需求也在增加(代码库不会无限期地停滞)。要问的一些问题是:项目已经提交了多少次?有多少贡献者?这个项目已经存在多久了?最后一次提交是什么时候?释放?稳定发布?有论坛或者用户群吗?
  • 受欢迎程度 :这更适用于低端和高端人群。如果没有人使用某样东西,就很难获得对图书馆的支持。该框架也更有可能失效并被抛弃。另一方面,如果某个东西广受欢迎,那么资源很容易获得,并且你可以确信这个框架将会有一个光明的未来。
  • 个人喜好 :你就是更喜欢。当所有其他事情都差不多的时候,你自己的直觉偏好很重要。

比较活动的一些项目度量的一个地方是在www.ohloh.net/p/compare,在那里你可以一次比较多达三个项目的统计数据。

可用框架

在这里,我选择了几个最有前途的框架(对我来说),并附有概要和网站地址。之后,我们将给出一个基本的“你好,世界!”两个框架的例子:GLGE 和 philoGL。

image 注意我看不出列出所有当前的框架有多大价值,因为许多框架缺乏之前指定的必要标准(支持、特性、用户),而这些标准是长期存在所必需的。当你试图选择一个来使用时,把它们都展示出来只会把水搅浑。在撰写本文时,这个列表包含了一些顶级的框架。这个列表有些主观,所以如果我遗漏了一个你认为有价值的框架,我很抱歉。

C3DL

C3DL 代表 C anvas 3D L 库。该库旨在提供“一组数学、场景和 3d 对象类,使 WebGL 对于那些希望在浏览器中开发 3D 内容但不想深入处理 3D 数学的开发人员来说更容易访问。”

C3DL 网页有几个教程和很好的文档。你可以在这里找到它:www.c3dl.org/

铜光〔??〕〔??〕??〕

CopperLicht 可用于 3D 应用和游戏。特性包括快速渲染、世界编辑器和许多模型格式的导入。你可以在这里找到更多关于它的信息:www.ambiera.com/copperlicht/index.html

GLGE

GLGE 的主要目标是简化。“懒人的 WebGL”和“GLGE 的目的是向 web 开发者掩盖 WebGL 的复杂本质,然后他们可以花时间为 web 创造更丰富的内容。”在本章的后面我们将展示一个 GLGE 的例子。你可以在这里找到 GLGE:【http://www.glge.org/】??

Jax

Jax 的设计考虑到了快速开发。它是“快速构建健壮、高质量 WebGL 应用的一站式商店”Jax 使用 Ruby 语言和模型-视图-控制器(MVC)模式来分离组件。一些内置功能包括键盘和鼠标输入处理,以及单元测试功能。你可以在 http://jaxgl.com/的找到 Jax 网站,在 https://github.com/sinisterchipmunk/jax 的找到 github 的源代码

kicks

KickJS 专注于用 WebGL 进行游戏开发。KickJS 还提供了一个在线交互式 GLSL 编辑器,如第二章中展示的那样,还有一个在线编辑器(目前处于测试阶段)。你可以在这里找到 KickJS 网站:【http://www.kickjs.org/】??,在 github 上托管的源代码在这里:【https://github.com/mortennobel/KickJS/】??

PhiloGL

PhiloGL 专注于数据可视化和游戏开发。该框架的目标是在编写时考虑到最佳的 JavaScript 实践,并对基本的 WebGL 调用进行抽象。在本章的后面我们将展示一个 philoGL 的例子。你可以在这里找到更多关于它的信息:www.senchalabs.org/philogl/

场景 ??

SceneJS 擅长渲染大量可拾取的对象,如工程和医疗应用中使用的对象。这是可能的,因为该框架(顾名思义)提供了一个场景图引擎,它在内部使用了一个高效的优化绘制列表,并且是基于 JSON 的。你可以在这里找到更多关于它的信息:www.scenejs.com/

TDL

Threedlibrary (TDL)专注于低水平的使用和性能,而不是易用性。谷歌身体和许多高性能的演示使用 TDL。你可以在这里找到更多关于它的信息:code.google.com/p/threedlibrary/

三. js

如前一章所述,Three.js 是一个通用的 3D 引擎,它抽象出了许多细节,使得开发 WebGL 更加容易。Three.js 是目前最流行的 WebGL 框架,有人认为 WebGL 和 Three.js 是一回事。事实并非如此,但正如前一章所展示的,这是一个很好的尝试框架。你可以在这里找到 github 上的 Three.js 库:github.com/mrdoob/three.js

在这一章,我们将展示如何导入网格和使用 Three.js 的物理引擎。

一句“你好,世界!”例子

philoGL 是来自 Sencha 实验室的一个框架,该项目的主要开发者是尼古拉斯·加西亚·贝尔蒙特。框架网站在www.senchalabs.org/philogl/,源代码在 github 上:github.com/senchalabs/philogl

首先,下载这个库,并将其包含在本地

,或从远程位置如

philoGL 库的\examples 文件夹展示了 Giles Thomas 在learningwebgl.com/blog/?page_id=1217的热门网站“学习 WebGL”的核心 WebGL 课程的 philoGL 版本。该库分为核心和模块。文件可在 http://www.senchalabs.org/philogl/doc/index.html获得

清单 8-1 显示了库包含的移植的“学习 WebGL”第 4 课示例的修改版本,供我们进一步分析。从清单中可以看出,philoGL 对 JavaScript 使用了非常面向对象的方法。为简洁起见,省略了网格数据,但可以在/08/01_philogl_cube.html 中找到完整的联机文件

清单 8-1 。 代码用 philoGL 旋转一个立方体

<title>Philo cube 试验</title>

九、调试和性能

在本章中,我们将展示如何排除错误并提高应用性能。我们将:

  • 展示用于调试 WebGL 代码和着色器的有用工具
  • 复习一些常见错误及其解决方法
  • 展示如何通过优化我们的代码来消除常见的瓶颈,从而提高 WebGL 的性能
  • 确定 WebGL 最佳实践

排除故障

当我们的程序产生错误的结果时,作为计算机程序员,我们说程序中有个错误或者表现出个错误 。识别 bug 错误来源并修复它们的过程被称为调试

为什么要以精通调试代码为目标?尽管调试通常是编程中最耗时和最令人沮丧的部分,但它也是开发中很自然的一部分。使用能够查明错误来源的工具和技术,以及对常见错误的了解,对于最小化我们花费在调试上的时间是至关重要的。

集成开发环境

我们应该寻求帮助的第一个地方是我们编码的地方。虽然我们可以使用没有语法高亮或代码智能的纯文本编辑器,但是我们为什么要这样做呢?大多数现代的 ide 会通过颜色和/或其他语法高亮显示提供近乎即时的反馈,并显示警告或通知。有很多很多的 ide 和文本编辑器可用,每一个都有各种各样的特性。有些是轻量级的,而有些是内存密集型的,有些是开箱即用的,而有些有插件或模块来添加功能。ide 的价格也从免费到非常昂贵不等。一些适合 JavaScript 和 web 开发的文本编辑器和 ide 包括 Sublime、Notepad++、Netbeans、Eclipse、WebStorm、Zend Studio、Aptana、Cloud9 和 Komodo。其中,有趣的是,Cloud9,顾名思义,是托管在云中的。没有本地安装,当然有利有弊。

至少,您的 IDE 或文本编辑器应该能够检测 JavaScript 和 HTML 语法,有一些颜色编码,可视化地匹配大括号和圆括号,有行号和搜索/替换支持。另一方面,ide 可以有内置的版本控制和远程文件支持、单元测试、重构、代码完成、API 智能等等。你可以在图 9-1 中看到一个 IDE 运行的例子。

9781430239963_Fig09-01.jpg

图 9-1 。Webstorm IDE 中的 jQuery 库自动完成功能

我不会试图说服您使用特定的 IDE 或文本编辑器。选择权在你,最终应该是你觉得最舒服和最有效率的使用方式。需要考虑的一些因素包括:

  • 积极发展和社区基础:不要花时间去学习那些很快就会过时或没人用的东西。
  • 能力和生产力:你能通过几个按键/宏做一些令人惊奇的事情吗?或者 IDE 的巨大实际上阻碍了你的生产力吗?请记住,IDE 的目的是通过提供有助于开发的工具来提高生产率和易用性。
  • 可扩展性:IDE 有插件、模块和第三方集成支持吗?
  • 直观性:IDE 是否设计良好,易于导航?
  • 可配置性:如果初始设置不符合您的要求,编辑器/IDE 中有多少是可定制的?
  • 资源使用和稳定性:加载 IDE 需要几秒钟还是几分钟;是否有求必应;是否占用过多内存;经常死机吗?
  • 焦点:IDE 是针对一种语言还是多种语言定制的?一个特定的任务还是很多?这些都有利弊。通常,如果一个 IDE 是为一种语言定制的,它会比为多种语言设计的软件更时尚、更优化,而且可能还拥有高级工具。然而,一个面向多种语言的 IDE 意味着如果你经常用多种语言编码,你只需要学习一个 GUI 。

浏览器开发工具

WebGL 在浏览器内部运行,使用的 API 是用 JavaScript 编写的。调试时我们应该寻求帮助的下一个地方是浏览器内部,因为每个主流浏览器都有自己内置的开发工具。这些开发人员工具的可用性和功能各不相同,但都具有相同的功能:查看和操作文档对象模型(DOM)、资源、网络流量的能力,以及输出 JavaScript 调试和错误信息的交互式控制台。

Chrome/Safari 都提供了开发者工具。Firefox 有 Firebug 和开发者扩展,Internet Explorer 有开发者工具栏,Opera 有蜻蜓。就内置支持而言,Internet Explorer 开发工具在版本 8 和版本 9 之间有了很大的改进。然而,在我看来,Chrome 和 Firefox 仍然是功能最丰富的浏览器工具。

默认情况下,Safari 禁用了他们的开发者工具。要启用它们,您需要转到首选项> >高级选项卡,然后单击“在菜单栏中显示开发菜单”复选框。火狐扩展可以在 http://getfirebug.com/和 ?? 获得。

在图 9-2 中,我们演示了如何通过 Chrome 开发者工具控制台标签交互地找到一个 DOM 元素。

9781430239963_Fig09-02.jpg

图 9-2 。使用 Chrome 开发者工具中的控制台搜索<标题>标签

在图 9-3 中,我们展示了 Opera 的蜻蜓开发者工具的网络流量标签。此标签显示网页已载入和正在载入的资源的时间线。

9781430239963_Fig09-03.jpg

图 9-3 。用蜻蜓歌剧院观看网络统计

在图 9-4 中,我们展示了 Firefox 的一个很酷的新特性:以三维方式可视化 DOM 的能力。WebGL 使这个工具成为可能。

9781430239963_Fig09-04.jpg

图 9-4 。火狐中 DOM 元素的 3D 可视化,由 WebGL 实现

另外两个特定的浏览器工具是:在 Firefox 的地址栏中输入“about:config”和搜索“webgl”可以让你调整 Firefox 的 webgl 设置。在 Chrome 中输入地址“about:tracing ”,就可以在浏览器中对 WebGL 应用进行分析。

发送到控制台的调试消息

为了向我们自己或其他开发人员输出关于程序状态的信息,或者如果有我们应该知道的警告或错误,我们可以将消息打印到 JavaScript 控制台屏幕上,而不是显示可能令人讨厌的非严重错误的警告框,或者用我们的状态更新来混淆 DOM 文档。如前所述,所有的浏览器都有开发者工具,包括用于输入和输出命令和消息的控制台屏幕。JavaScript 中还有一个控制台对象,它有向开发人员控制台输出消息的方法。

例如,我们可以用 JavaScript 代码将日志消息和错误消息写到开发人员控制台,如下所示:

var myvar = 42

console.log("只是一些有帮助的信息");//只是一些有用的信息

console.error("更严重的东西:"+myVar);//更严重的东西:42

控制台中显示的这两条消息的主要区别在于,错误消息通常是红色字体,而日志消息是黑色的。控制台对象有更多的方法,Firebug 控制台的应用编程接口(API)可在getfirebug.com/wiki/index.php/Console_API获得。尽管控制台对象的具体实现是依赖于浏览器的,但是其他的(比如基于 WebKit 的)支持 Firebug 使用的大部分实现。

查看别人的代码

因为 WebGL 使用的是 JavaScript 编写的客户端 API,所以我们可以很容易的查看别人的代码。这样做可以洞察我们以前可能没有考虑过的技术。查看 JavaScript 代码有两种方法:右键单击并访问菜单项“查看页面源代码”或“查看源代码”,或者查看开发人员工具的资源选项卡。对于着色器源,我们也可以查看原始源。然而,正如我们在第七章中所展示的,框架可以修改最终的着色器源。因此,使用像 WebGL inspector 这样的工具会更有用,我们将在本章的后面介绍。

在线沙盒

沙箱 是一个安全的环境,众所周知,孩子们可以在里面玩耍,发挥他们的想象力。用开发术语来说,沙箱是一个隔离的测试环境,我们可以在不损害生产代码的情况下摆弄我们的代码。

网上有很多地方我们可以快速安全地修改 JavaScript 代码。http://jsfiddle.net/的 jsFiddle 允许你运行 JavaScript 代码(可选 HTML 和 CSS)并查看输出。它提供了一个简单的选择框来切换普通 JavaScript 库的包含,并集成了 JSLint 支持来检查代码的有效性。与 jsFiddle 类似的站点是 JS Bin 及其位于 http://jsbin.com的站点

着色器可以在几个网站上在线修改,如 KickJS(可以在www . KickJS . org/example/shader _ editor/shader _ editor . html找到)。我们在第二章中介绍了在线着色器工具,一些额外的站点在附录 D 中列出。

在线沙盒的主要用途是能够在为您配置的大部分环境中快速测试少量代码,并且能够安全地将您的代码作为链接共享给其他开发人员查看、调整和协作。

通用 WebGL 错误代码

WebGL 的一个问题使得调试相当困难,那就是只有五个主要的错误代码(包括表示没有错误的代码)。这些代码是数字常量。WebGL 规范的一个例子是:

const glen um INVALID _ ENUM = 0x 0500;

主要错误代码有:

  • 没有错误- 我们准备好了
  • INVALID_ENUM- 为枚举参数指定了不可接受的值
  • INVALID_VALUE- 数值参数超出范围(例如试图指定着色器位置-1)
  • INVALID_OPERATION- 指定的操作在当前状态下是不允许的(比如试图生成一个没有绑定纹理的纹理贴图)
  • 内存不足- 应用已耗尽内存

WebGL 的许多函数调用都存在主要错误。这使得我们能够准确地触发和检测错误发生的位置和时间变得至关重要。有关每个函数可能抛出的错误代码的完整列表,请参考 WebGL 规范。

我们还可以检查许多 WebGL 状态,例如当我们使用以下命令检查帧缓冲区状态时:

GLenum checkFramebufferStatus(GLenum 目标);

在其他可能的返回值中,我们可能会收到以下内容:

frame buffer _ INCOMPLETE _ ATTACHMENT//0x8cd 6

上下文错误

与 HTMLCanvasElement 关联的 WebGL 呈现上下文在创建时或在应用的整个生命周期中可能会有错误。我们现在将展示如何检查这些错误,并在遇到错误时适当地处理它们。

上下文创建

如果在我们尝试获取 WebGL 上下文时请求失败,浏览器需要向画布触发一个名为“webglcontextcreationerror”的 WebGL 上下文事件。为了监听这个事件,我们可以添加一个监听器,如 WebGL 规范示例 VII 所示,如清单 9-1 所示。

清单 9-1。 检查上下文创建错误

是 errorInfo = '。

函数 onContextCreationError(事件){

帆布. removeEventListener(

" webglcontextcreationerror ",

onContextCreationError, false);

error info = e . status message | " unknown ";

}

canvas.addEventListener(

" webglcontextcreationerror ",

onContextCreationError, false);

var GL = canvas . get context(" experimental-web GL ");

if(!gl) {

alert("无法创建 WebGL 上下文。\ n 原因:"+

errorInfo(错误信息):

}

清单 9-1 中的代码创建了一个错误监听器,试图获取一个 WebGL 上下文,如果有错误将显示原因,然后移除错误事件监听器。添加侦听器的好处是,我们可以深入了解无法创建上下文的原因。

上下文丢失和恢复

如果浏览器丢失了 WebGL 的上下文,我们可以检测到并恢复它。然而,任何资源,如纹理或缓冲区,将需要重新创建。上下文可能会因为移动电源事件、GPU 重置、客户端放弃背景标签或资源不足而丢失。来自 WebGL 规范的示例 VI 的一部分显示在清单 9-2 中,并演示了如何监听“webglcontextlost”和“webglcontextrestored”事件。

清单 9-2。 倾听失去的语境,还原语境

canvas.addEventListener(

" webglcontextlost ",函数(事件){

//通知 WebGL 我们处理上下文恢复

event.preventDefault();

//停止渲染

window.cancelAnimationFrame(必需):

},假);

canvas.addEventListener(

" webglcontextrestored ",函数(事件){

initialize resources();

},假);

在清单 9-2 中,我们注册了上下文丢失和恢复的监听器。如果上下文丢失了,我们就停止制作动画。在恢复时,我们重新装载我们的资源。

image 注意上下文丢失是 WebGL 的主要安全问题之一,OpessnGL GL_ARB_robustness 扩展旨在为应用添加检测丢失上下文的能力。这将有助于显卡“监视”拒绝服务等恶意攻击。

持续检查错误

在开发过程中,我们可以使用由 Khronos 集团(监督 webgl 的联盟)创建的 webgl-debug.js 库,该库可从CVS . Khronos . org/SVN/repos/registry/trunk/public/web GL/SDK/debug/web GL-debug . js获得。http://www.khronos.org/webgl/wiki/Debugging概述了使用方法。这个库将在每次 WebGL 调用之间调用 getError,并将错误结果输出到控制台。我们可以通过调用以下命令将错误号转换成可读性更好的字符串:

webldbugutils . glenumosting(GL . get error());

调用 getError 的开销很大,因为它会轮询 GPU,从而有效地阻止 WebGL API 和 GPU 之间的进一步通信,直到返回结果。因此,该库不应用于生产代码中。

在本地下载 webgl-debug.js 文件。从第三章的 03/texture_and_lighting.html 文件开始,我们会稍微修改一下代码来利用这个库。首先,我们包括新的脚本文件:

现在让我们产生一个错误,这样我们就可以演示这个库了。在 setupWebGL 函数中,将深度测试的启用从 gl.enable(gl。DEPTH_TEST)到 gl.enable(gl。DEPTH_TEST_FOOBAR)。如果我们运行这个程序,它看起来很奇怪,但是我们在控制台中没有得到任何指示,表明有一个 WebGL 错误,如图 9-5 所示。

9781430239963_Fig09-05.jpg

图 9-5 。我们的控制台没有产生错误

现在,我们将在 initWebGL 函数的 webgl-debug.js 库中包装我们的 WebGL 上下文:

如果(总帐)

{ GL = webldbugutils . make debug context(GL);

结果(在 09/texture _ and _ lighting _ debug . html 文件中找到)是不产生图像,但是有用的调试信息被输出到控制台。它准确地告诉我们哪个函数和行是错误的 setupWebGL 函数中的第 132 行——如图 9-6 所示。

9781430239963_Fig09-06.jpg

图 9-6 。来自 webgl-debug.js 的调试信息

WebGL 检查器

目前可用的最好的浏览器内调试工具是 WebGL Inspector,它是一个有用的工具,可以查看视图着色器程序信息、加载的纹理、应用的当前状态、缓冲区的内容、捕捉快照以及帧的完整跟踪数据等等。WebGL inspector 由 Ben Vanik 和 James Darpinian 编写,可从benvanik.github.com/WebGL-Inspector/获得。它被宣传为

“一个高级 WebGL 调试工具包…受 gDEBugger 和 PIX 的启发,目标是使高级 WebGL 应用的开发更容易。就像 Firebug 和开发者工具之于 HTML/JS,WebGL Inspector 之于 WebGL。”

WebGL inspector 可以通过在网页中嵌入脚本或安装 Chrome 扩展来使用。安装完成后,带有 WebGL 内容的页面会在地址栏显示一个 GL 图标,并在网页上显示两个按钮,“捕获”和“UI”,如图图 9-7 所示。

9781430239963_Fig09-07.jpg

图 9-7 。WebGL 检查器的缓冲区标签

在图 9-7 中,显示了缓冲区选项卡,显示了我们的顶点缓冲对象(VBOs)的内容。我们可以使用纹理选项卡来确保我们的纹理已经正确加载,查看过滤器和夹紧参数,以及其他关于纹理的信息(见图 9-8 )。

9781430239963_Fig09-08.jpg

图 9-8 。WebGL 检查器的纹理数据

“程序”标签将显示我们程序的状态,以及属性和制服以及我们的顶点和片段着色器源代码,如图 9-9 所示。

9781430239963_Fig09-09.jpg

图 9-9 。WebGL 检查器的“程序”标签

状态选项卡显示了我们所有可调整的状态设置,如是否启用混合、混合颜色、透明颜色、多边形正面的方向等等,如图 9-10 所示。

9781430239963_Fig09-10.jpg

图 9-10 。WebGL 检查器的“状态”选项卡中显示的 WebGL 状态设置

时间轴选项卡将显示各种指标的实时数据,如帧时间、图元/帧和缓冲存储器。时间线是这个非常有用的程序的一个方面,它可以使用一些工作来产生更可扩展和可读的结果(见图 9-11 )。

9781430239963_Fig09-11.jpg

图 9-11 。WebGL 检查器的时间线指标

到目前为止,我们还没有讨论捕获按钮。在我看来,这是 WebGL inspector 最有用的功能。当您点击该按钮时,WebGL inspector 将捕获屏幕,并生成一个完整的帧轨迹,如图 9-12 中的轨迹选项卡所示。我们的示例程序很小,只有 19 行,但是复杂的 WebGL 应用可以有数千行,我们将在后面演示。突出显示为黄色的行是多余的。这是帮助提高性能的重要信息,如下一节所示。您可以选择不突出显示多余的电话,但可能会发现反馈很有用。WebGL inspector 还可以让我们减慢或暂停帧的推进。

9781430239963_Fig09-12.jpg

图 9-12 。“跟踪”选项卡显示使用 WebGL 检查器捕获的帧

在图 9-12 的第 19 行,右边有两个图标。第一个带有向右的箭头,让我们从单个 draw 命令运行独立的输出。在我们的示例应用中,这是整个场景。然而,在有几个绘制调用的更复杂的应用中,显示场景的特定部分被渲染是非常有用的。当第二个链接(看起来像一个 i )被点击时,一个新的窗口将弹出完整的绘图信息。该弹出窗口很大,首先显示了绘制的元素的网格,可以用鼠标滚轮放大和缩小,并在按住鼠标按钮的同时旋转。这个网格对于视觉上确认我们的顶点已经以正确的顺序渲染是有用的,这样我们也可以确保我们的多边形面的一致缠绕;红色在顺时针和逆时针方向的亮度是不同的。我们还可以显示使用的纹理坐标网格。接下来,显示程序统一和当前值的列表,后跟属性。最后,我们看到了 WebGL 设置的状态:顶点,片段,深度/模板和输出。该弹出窗口的第一部分如图 9-13 所示。

9781430239963_Fig09-13.jpg

图 9-13 。有关 WebGL 检查器中特定绘图调用的信息

如果我们点击显示在轨迹日志右侧的图像像素(未在图 9-12 中显示),我们将在一个新的弹出窗口中获得关于颜色成分以及如何获得最终像素颜色的所有信息。我们的例子没有混合,所以最终的颜色计算很简单,如图图 9-14 所示。但是,当与非透明 alpha 值混合时,此信息可能非常有用。

9781430239963_Fig09-14.jpg

图 9-14 。WebGL 检查器的像素历史

最后,WebGL inspector 对于指出错误也很有用。重新添加 gl.enable(gl。DEPTH_TEST_FOOBAR)线使轨迹的错误线以红色高亮显示,如图图 9-15 所示。

9781430239963_Fig09-15.jpg

图 9-15 。无效关键字导致的 WebGL 检查器错误

如果我们试图获得一个不存在的属性位置(例如,试图获得错误输入的属性 aVertexPosition2 而不是 aVertexPosition),getAttribLocation 返回(-1),这是一个无效值(参见图 9-16 )。

9781430239963_Fig09-16.jpg

图 9-16 。属性位置值无效导致的 WebGL 检查器错误

image 注意如果一个变量确实存在于你的顶点着色器中,但你从未使用过它,编译器会将其标记为未使用,并在编译和链接你的程序时将其移除。如果您稍后尝试获取它的位置,您将收到(-1)并产生相同的错误。

最后,假设我们调用了 gl.bindBuffer,将 null 值绑定到 WebGLBuffer 参数。这很容易发生,例如,在从文件中生成或读取数据时将数据写入变量,但在绑定已经初始化为 null 且从未写入的时使用不同的变量。突出显示的错误如图 9-17 所示。

9781430239963_Fig09-17.jpg

图 9-17 。将 VBO 绑定到 null 导致 WebGL 检查器错误

正如您所看到的,WebGL inspector 是一个具有多种用途的工具,我敦促读者熟悉它——您会感谢自己的。

使用 glsl 装置进行测试

单元测试代码——将程序的各个部分隔离成小单元,并在每个单元上运行自动化测试——是一种有价值的方法,可以确保程序按照预期的方式运行,并在重构时检测代码中的错误(对代码进行结构性而非行为性的更改,以改进代码设计和质量)。

http://code.google.com/p/glsl-unit/有一个相当新的 GLSL 单元测试框架。要克隆 git 存储库,您可以使用:

git clone

code.google.com/p/glsl-unit/

常见陷阱

使用 WebGL 编程时,有一些错误比其他错误更容易遇到。这里有一些要避免的陷阱。

缓存内容

没有使用文件更改。浏览器仍然在使用旧版本。使用 Shift-F5 进行硬浏览器刷新,或者通过重命名资源文件或故意向着色器程序或 javascript 文件添加错误(临时)来引起浏览器的注意。

为不兼容的上下文重用画布

“2D”和“webgl”上下文不兼容。尝试使用一个然后用另一个调用 canvas.getContext 将返回 null,而不是获得有效的上下文。

移动设备碎片精确支持

WebGL 只要求片段着色器浮点值支持 mediump。许多手机和移动设备只支持这种精度。如果您的目标是移动用户,请不要使用 highp。我们还可以通过调用 getShaderPrecisionFormat 函数来轮询设备支持的精度。这可以让您根据设备功能提供不同的着色器。

摄像机视角朝向错误的方向

确保虚拟摄像机指向场景的正确方向,并且顶点位于剪辑空间和视口内。

质地问题

尝试生成纹理贴图时使用非 2 次方(NPOT)纹理是一个致命错误。可用的纹理单元的数量也有限制。如果你运行图 9-18 所示的 webglreport,你可以很容易地看到你当前的浏览器和 GPU 支持的确切数字。

9781430239963_Fig09-18.jpg

图 9-18 。使用 webglreport 查看您的浏览器 WebGL 支持

性能因 GPU 而异

GPU 有不同的硬件设置和优化。在一个 GPU 上优化的东西在另一个 GPU 上可能非常慢。

加载错误的资源

检查是否加载了正确的文件,无论是纹理图像、网格还是着色器文件。还要确保没有违反跨原始资源共享规则。

浏览器差异

建议在调试时在不同的浏览器中尝试您的代码,因为结果可能会有所不同,或者只在某些浏览器中有效。原因是一些 WebGL 规范是依赖于客户端的。有最低要求,但不是所有的实现都是一样的。也不是所有的扩展都受支持。要轮询浏览器支持的可用扩展列表,您可以使用以下函数:

domstring[]getsupportxtenderextensions()

对象 getExtension(DOMString 名称)

getSupportedExtensions 函数返回一组受支持的扩展名。返回的数组中的每个字符串都将从 getExtension 返回一个有效的对象,而不在受支持的数组中的任何字符串名将返回 null。返回的对象表示扩展已被正确启用,但不需要包含任何函数或常数。

我们还可以使用函数 getParameter(GLenum pname)来查找其他浏览器支持信息,例如使用以下命令检查支持的最大纹理大小:

GL . get parameters(GL)。MAX_TEXTURE_SIZE)。

即使没有 WebGL 浏览器差异,也有 JavaScript 浏览器差异需要测试。例如,JavaScript 对象或数组中的尾随逗号在大多数浏览器中是正确的,但在 Internet Explorer 中是错误的。

ie) [1,2,3,]在 ie 中是不好的,而[1,2,3]在所有浏览器中都是好的。

而{"a":"1 "," b":"2 ",}在 IE 中是不好的而{"a":"1 "," b":"2"}在所有浏览器中都是好的。

analyticalgraphicsinc.github.com/webglreport/有一个很好的查看浏览器 WebGL 常量的实用程序,Chrome 输出示例如图图 9-18 所示。

外部着色器解析错误

当从外部文件加载着色器时,您可能会注意到一个似乎有效的语句,如以下任一项:

if(a < b){;}

if(a & & b){;}

每一行都会导致着色器无法加载,因为 XML 实体“

if(a<b){;}

if(a&&b){;}

或者,您可以调整从对 HTML 的 Ajax 调用返回的数据类型,然后用 jQuery 解析出脚本标记:

$.ajax({

异步:假,

url: './my_shader.fs ',

成功:函数(数据){

fs_source = $(数据)。html();

},

数据类型: 'html'

});

性能

对于简单的应用,遵循 WebGL 最佳优化实践并不重要。浏览器和 GPU 可以非常快速地执行计算,因此少量的绘制调用将快速渲染,并以良好的帧速率出现,无论我们的代码是否经过优化。然而,随着我们的应用变得越来越复杂,并涉及更多的 WebGL 和 GPU 交互,它们将很快变慢,其影响范围从轻微的明显到令人虚弱的使用。对我们来说幸运的是,有已知的方法来获取现有的代码并优化它。

测量帧速率

为了查看我们所做的是否真的改善了处理,我们需要测量我们正在渲染的帧率 (每秒的帧数)。较低的帧速率会显得起伏不定,而较高的帧速率会显得平滑自然。帧速率通常以每秒帧数(fps)来衡量。由于相机是手摇的,无声电影的帧速率可变,约为 14-26 fps。早期的投影仪将 fps 设置为恒定的 24 fps,这当然显得更加平滑。一些较新的电影使用 48 fps,计算机显示器的刷新率通常为 60 赫兹(Hz,赫兹,是每秒的周期数),尽管现在更大的数字显示器超过 100 赫兹。所以你能达到的帧率越高越好。

为了测量帧率,我们将使用位于 https://github.com/mrdoob/stats.js 的 Github 上的 stats.js 库。这个库是 three.js 框架的作者写的,这个框架在第七章中有介绍。下载 stats.js 文件,并将其包含在代码中,如下所示:

接下来,我们需要将 stats

附加到我们的文档,并每次通过 requestAnimationFrame 循环调用它的 update 方法。请注意,如果我们的场景暂停,则不会调用 update 方法。这是个人偏好,否则当应用暂停时,fps 将会波动到一个更高但不相关的值。使用 stats.js 库的代码如清单 9-3 所示。

清单 9-3。 向我们的应用添加统计计算器

var Stats = new Stats();

函数 initWebGL()

{

attach stats();

(函数 animLoop(){

如果(!暂停){

setupwbgl();

setMatrixUniforms();

draw scene();

stats . update();

}

requestimationframe(aniloop,canvas);

})();

}否则{

alert("错误:您的浏览器似乎不支持 WebGL。");

}

}

函数 attachStats()

{

stats . getdomelement()style . position = ' absolute ';

stats . getdomelement()style . left = ' 0px ';

stats . get item(). style . top = ' 0px ';

document . body . appendchild(stats . getdocument());

}

image 注意:你可以通过使用 setMode 方法查看渲染帧而不是 fps 所用的毫秒数:

stats.setMode(1);0: 满分频, 1: 毫秒

统计部件显示在图 9-19 的左上角。

9781430239963_Fig09-19.jpg

图 9-19 。显示 stats.js 的 fps 指标

当我们使用 stats.js 并打开多个浏览器选项卡时,如果我们切换到不同的选项卡,然后返回,当我们返回时,帧速率会急剧下降。这很好,因为它表明 requestAnimationFrame 正在如承诺的那样工作,并且没有执行不必要的动画。

优化的复杂性

很难确定如何优化 GPU,因为有许多不同的硬件实现,一些有助于某些 GPU 的优化实际上会影响其他 GPU 的性能。

瓶颈

为了优化代码,您通常需要找到瓶颈——系统性能最受限制的地方——并修复它们。

很多人都熟悉的一个例子是洗衣服和烘干衣服。假设你有三台洗衣机和一台烘干机。每台洗衣机每次洗衣需要 30 分钟,烘干机需要两倍的时间:每次洗衣需要 60 分钟。洗衣机和烘干机的容量是一样的。我们需要等待干燥机完成,并受到所用时间的限制;这是我们系统的瓶颈。

假设我们有三车衣服要洗。进行 3 次洗涤的总时间为 210 分钟(并行洗涤 30 分钟+每次干燥 60*3)。我们可以通过以下方式限制或消除瓶颈来提高系统性能:

  • 减少干燥时间
  • 增加机器的生产能力
  • 购买更多干衣机

有了第一个改进,假设你可以摆弄机器,把烘干时间缩短到 45 分钟。3 次装载的总时间为 165 分钟(30 + 45*3)。

对于第二个改进(但没有速度改进),假设您可以修改烘干机,以处理两个完整的负载,而不是一个。您仍然需要进行 2 次完整的干燥机循环(1 次半容量= 1 台洗衣机负载,然后 1 次全容量= 2 台洗衣机负载),但总时间减少到 150 分钟(30 + 60*2)。

对于第三个选项,如果您可以购买两台额外的机器,您的总时间将减少到 90 分钟(30 + 60)。

在洗衣机/干衣机的例子中,购买更多的硬件(类似于拥有更多的计算能力或 RAM)会带来最大的改进。然而,在其他情况下,瓶颈可以通过更有效的算法来改善。

例如,如果您有一个需要 1000 个数字的计算,并且该计算的当前复杂度以 n 3 的顺序增加,则需要 10 亿个计算单元才能完成。如果您购买 4 台机器并在它们之间分配计算,每台机器仍需要 250,000,000 次计算。然而,如果你可以在不购买任何新东西的情况下,将算法的复杂度降低到 n 2 ,你就减少了 1000 到 1000000 的计算成本。

WebGL 瓶颈

虽然优化不是绝对的,但是有一些通用的最佳实践指南,可以最大限度地提高性能并限制瓶颈。昂贵的操作包括阻止浏览器和 GPU 的通信以及不必要的计算和查找。片段着色器要执行的计算最多,因为它对场景中的每个像素都进行操作。由于这个原因,片段着色器也经常成为应用中的瓶颈。

片段着色器

片段着色器作用于每个像素。因此,这是计算瓶颈和性能损失的可能来源。判断片段着色器实际上是否是瓶颈的一种方法是减小画布的大小并比较帧速率。如果有显著的性能改进,那是因为需要计算的像素更少了,所以您应该尝试优化片段着色器。

一个技巧是做相反的事情:片段着色器完成后,将画布拉伸到更大的尺寸。这不需要更多的 GPU 计算,应该是一个相对便宜的客户端操作。当然,这只有在拉伸产生可接受数量的伪像或锯齿标记时才是可行的(也就是说,它看起来仍然很好)。

浏览器资源

甚至在我们开始渲染我们的场景之前,我们需要加载资源。有几种方法可以减少资源的物理大小,从而缩短网页的初始加载时间。

缩小 JavaScript 文件

当我们的代码为生产做好准备时,我们可以将多个文件组合起来并缩小成一个压缩文件。有很多工具可以做到这一点,从直接剪切/粘贴源文件到上传文件:

www.minifyjavascript.com/

jscompress.com/

要使用命令行:

html5boilerplate.com/docs/Build-script/

developer.yahoo.com/yui/compressor/

纹理

我们应该保持纹理尺寸尽可能的小。如果一个较小的 128 x 128 的纹理看起来和一个较大的 512 x 512 的几乎一样,我们应该使用它。内存会小 16 倍。其次,我们应该选择一个合适的图像格式。BMP 图像通常比 png 大,png 比 JPEGss 大,JPEG 比 WEBPs 大。您选择哪种格式还取决于您是否需要 alpha 通道,以及您可以承受多少图像数据丢失,同时仍能获得令人满意的图像质量。

浏览器与 GPU 能力的对比

GPU 的计算速度比客户端的 JavaScript 快几个数量级。在 GPU 上,许多许多操作可以并行完成,并使用编译后的本机代码。因此,如果可能的话,任何“重担”都应该由 GPU 来承担。

阻塞 GPU

GPU 从与顶点属性相关联的应用获取数据流。这些流然后到达顶点处理器,然后到达片段处理器。GPU 并行计算,但 JavaScript API 和 GPU 之间的通信更串行。自然,我们不希望尽可能地阻止这个浏览器与 GPU 的通信。这样做将导致我们的程序似乎停滞不前,帧速率下降。那么我们能做些什么来限制不必要的浏览器与 GPU 通信呢?

批量提取调用

我们应该尽可能地限制 draw 调用(drawArrays,drawElements ),方法是将它们批处理在一起。GPU 可以轻松地同时处理数百或数千个三角形。但是,单个 VBO 的大小也有上限。

image 注意: Three.js 有一个工具可以合并不相交的几何图形,以减少所需的单独绘制调用的数量。详细信息在/src/extras/GeometryUtils.js 文件和函数 THREE.GeometryUtils.merge 中。

减少缓冲切换

我们应该将顶点属性(如颜色、法线和位置)组合成交错数组,而不是使用单独的 vbo。我们将在本章的后面讨论交错阵列。

减少着色器程序切换

如果我们有几个正在使用的着色器程序和几个对象,如果可能的话,我们希望对使用每个着色器的元素进行分组,以便我们可以限制我们需要更改活动着色器程序的频率。

缓存状态变化(gl.getX/gl.readX)

每次需要轮询 WebGL 的一个状态组件时,浏览器都需要中断 GPU 获取信息。要尽量避免的一些调用有 getAttachedShaders、getProgramParameter、getProgramInfoLog、getShaderParameter、getShaderInfoLog、getShaderSource、getTexParameter、getParameter、getError、getActiveAttrib、getActiveUniform、getattrib、getUniformLocation、getVertexAttrib、getVertexAttribOffset、getTexParameter、getRenderbufferParameter、getFramebufferAttachmentParameter、getBufferParameter parameter 以及关联的 set X 调用。如果可能的话,我们应该用 JavaScript 存储这些信息的缓存版本。我们还希望限制统一更改,因为它们需要与 GPU 进行交互。要限制的其他 WebGL 调用是 readPixel 和 finish。

不要在生产中使用 getError

如上所述,使用 getError 查询昂贵的 GPU。在开发过程中持续使用它,但不要在代码进入生产环境后使用它。

移除多余的呼叫

我们已经展示了 WebGL inspector 如何非常有助于向您展示不必要的 API 调用。不必要调用的一个例子是在没有任何改变的情况下每帧设置一个状态。这可以通过将特定的状态设置代码移动到呈现循环之外的初始化函数中来补救。

另一个不必要冗余的例子是通过重新计算每个球体的顶点来生成 1000 个球体,然后在场景中置换它们。相反,计算一次单位长度(1 = x² + y² + z²)球体的顶点并存储它们。然后对于其他球体,缩放和变换所有生成的点以产生方差。这大大减少了所需的三角运算的数量,并用成本低得多的乘法和加法的初等运算来代替它们。

限制活动纹理更改

我们可以通过将小纹理合并成一个更大的纹理来减少改变活动纹理的频率。这个合成图像被称为纹理图谱 。一些行星、太阳和月亮的纹理图谱如图 9-20 中的所示。

9781430239963_Fig09-20.jpg

图 9-20 。月球、太阳和一些行星的纹理图谱

我们将在本章的后面使用这个纹理贴图来优化性能。为了提高性能,还应确保生成第三章中概述的 mipmaps。

用进废退

如果您没有使用混合或深度测试等功能,请禁用它们。例如,如果您只渲染到二维而没有 MVP 变换,或者如果您正在渲染 2D 对象,而您知道这些对象是按照从最远到最近的顺序绘制的,那么您可以安全地禁用深度测试。

更快的阵列

WebGL 数组自然比传统的 JavaScript 数组快,因为它们利用了新的 JavaScript 类型化数组。将此与交叉存取数组的使用相结合将提高 VBO 和属性性能,我们现在将对此进行解释。

类型化数组

传统上,JavaScript 中传输的原始数据被视为字符串。由于 WebGL 将大量数据传递给 GPU,因此使用类型化数组来提高性能。类型化数组使用原始二进制数据,并具有固定的字节大小和类型,这提高了流效率。

WebGL 使用原始大小:

gl .字节- 1 字节

gl。SHORT - 2 字节

gl。浮点型- 4 字节

JavaScript 中可用的类型化数组有以下几种:

ArrayBuffer, ArrayBufferView, DataView

浮动 32 阵列,浮动 64 阵列

int 16 阵列,int 32 阵列,int 8 阵列

Uint16Array, Uint32Array, Uint8Array

WebGL 中需要类型化数组。你可以在 https://developer.mozilla.org/en/JavaScript_typed_arrays 查看更多关于他们的信息。

交错数组

切换 VBOs 是昂贵的。为了简单起见,属性数据通常是分开的。然而,我们可以将部分或全部数据合并到一个交错数组中,而不是使用单独的颜色、纹理、位置和法线数组。这在性能方面会好得多,因为阻碍性能的不是一次传递给我们 GPU 的数据量,而是所需的单独绘制调用的数量。

交错数组只是在每个顶点将数据混合在一起。在图 9-21 中,数组数据的每一行都有 RGBA 颜色数据,后面是 XYZ 位置数据(W 被省略)。每行的元素总数是 7:RGB axyz。当我们告诉 WebGL 如何解释我们的数据时,将需要大小和顺序。WebGL 不关心数据的实际内容,由我们来提供数据的适当上下文。我们可以按照 XYZRGBA 的顺序有效地交错数组。

9781430239963_Fig09-21.jpg

图 9-21 。使用单独的数据阵列与交叉存取阵列

让我们看看使用交错数组所需的实际代码。在清单 9-4 中,我们展示了两个属性数组的数组声明,以及它下面的一个交错数组的声明。

清单 9-4。 分离位置和颜色数组和交错数组

//带有分隔数组的正方形

var vertexPositionArray = [

10.0, 10.0, 0.0,

10.0, -10.0, 0.0,

-10.0, -10.0, 0.0,

-10.0, 10.0, 0.0

];

var verticalarray =[

1.0, 0.0, 0.0,

0.0, 1.0, 0.0,

0.0, 1.0, 1.0,

0.0, 0.0, 1.0

];

//带有交错数组数据的正方形

var vertexInterleavedArray = [

//x,y,z,r,g,b

10.0, 10.0, 0.0, 1.0, 0.0, 0.0,

10.0, -10.0, 0.0, 0.0, 1.0, 0.0,

-10.0, -10.0, 0.0, 0.0, 1.0, 1.0,

-10.0, 10.0, 0.0, 0.0, 0.0, 1.0

];

现在我们有了数据,我们可以将它绑定到一个缓冲区,然后在稍后绘制场景时将我们的属性指向缓冲区。清单 9-5 中的显示了单独的缓冲器。

清单 9-5。 绑定单独的缓冲区并稍后指向属性

//用于位置和颜色数据的两个缓冲区

var vertexpositionbuffer = GL . create buffer();

gl.bindBuffer(gl)。ARRAY_BUFFER,vertexPositionBuffer:

gl.bufferData(gl。ARRAY_BUFFER,new float 32 ARRAY(vertexPositionArray),gl。STATIC _ DRAW);

var verticalrbbuffer = GL . create buffer();

gl.bindBuffer(gl.ARRAY_BUFFER,顶点颜色缓冲区);

gl.bufferData(gl。ARRAY_BUFFER,new float 32 ARRAY(vertexcoloraray),gl。STATIC _ DRAW);

gl.bindBuffer(gl)。ARRAY_BUFFER,vertexPositionBuffer:

gl . verticalpointer(vertexplicationattributes,3,GL)。浮点,false,0,0;

gl.bindBuffer(gl.ARRAY_BUFFER,顶点颜色缓冲区);

gl . verticalpointer(verticalcolor 属性,3,GL)。浮点,false,0,0;

vertexAttribPointer 的最后两个参数是步幅偏移。这些以字节为单位,默认值都是 0。

进展

stride 让 WebGL 知道数组中每行顶点数据相隔多远。所以对于 vertexPositionArray,这是 3 *Float32Array。字节每元素= 12。以下语句将产生与使用默认步幅 0 相同的结果。

gl . verticalpointer(vertexplicationattributes,3,GL)。浮点值,false,12.0;

对于我们的交错阵列,我们的跨距为 6 * float 32 阵列。每元素字节数= 24。请注意,我们还可以使用一个包含“垃圾数据”的数组,这些数据是我们在每行中没有使用或稍后使用的,例如://[r,g,b,x,y,z,some _ extra _ value]在这种情况下,跨距将是 7 * Float32Array。BYTES_PER_ELEMENT = 28,即使我们仍然只使用每行的 24 个字节。

抵消

偏移量告诉 WebGL 从哪个字节开始读取数据。对于清单 9-4 中的 vertexPositionArray,如果我们想要丢弃前两个数字并从第三个开始,我们将使用偏移量 2 * Float32Array。每元素字节数= 8。我们的前三个顶点是:

(0.0,10.0,-10.0)

(0.0,-10.0,-10.0)

(0.0,-10.0,10.0)

当我们需要指向交错数组中的特定数据属性时,stride 和 offset 真正派上了用场。使用相同的缓冲区,我们可以将属性的偏移量设置为不同的适当值。对于清单 9-4 中的交错数据,位置数据没有偏移,而颜色数据是 3 * Float32Array。BYTES_PER_ELEMENT = 12 字节,所以我们将其偏移量设置为这个值,如清单 9-6 所示。

清单 9-6。 使用单个缓冲器进行数据间插

//使用单个缓冲区的交错数据

var vertxinterleave dbuser = GL . create buffer();

bindBuffer(gl。ARRAY_BUFFER,vertexInterleavedBuffer);

gl.bufferData(gl。ARRAY_BUFFER,new float 32 ARRAY(vertexInterleavedArray),gl。STATIC _ DRAW);

bindBuffer(gl。ARRAY_BUFFER,vertexInterleavedBuffer);

gl . verticalpointer(vertexplicationattributes,3,GL)。浮点值,false,24.0;

gl . verticalpointer(verticalcolor 属性,3,GL)。浮点,false,24,12;

索引缓冲区

如果可能的话,使用索引缓冲区,因为 GPU 针对它们的使用进行了优化。索引缓冲区允许我们重用顶点,因此需要在 CPU 和 GPU 之间传输更少的数据。

早期估算计算

您不会在每次需要使用圆周率时都计算到小数点后 100 位,而是使用预先计算的值。此外,圆周率精确到 100 位小数很可能是不必要的,并且不会在最终结果中产生任何差异。这展示了计算中的两个重要概念:

  • 最快的计算是不需要进行的计算。
  • 如果(视觉)结果大致相等,估计和简化往往比精确更好。

重用预先计算的值比多次执行计算要好。例如,如果每个顶点计算 cos(时间),在我们的 JavaScript 中每帧计算一次这个值,并作为统一值传递给顶点着色器,这比每个顶点都计算一次要好得多。

按照从最便宜到最贵的顺序:

  • 在代码内部设置为常量的外部计算
  • 应用设置期间完成的计算
  • 计算重做了应用的每一帧
  • 在顶点着色器中对每个顶点进行计算
  • 对片段着色器中的每个像素进行计算

当然,我们还必须考虑到 GPU 比 JavaScript 强大得多,所以在有些情况下,早期用 JavaScript 进行复杂的计算与在 GPU 中多次进行计算一样糟糕,如果不是更糟糕的话。

最佳实践总结

以下是被广泛认为是 WebGL 最佳实践用法的技术:

  • 尽可能批量,减少抽签调用的次数
  • 交错属性数据
  • 减少状态变化查询
  • 不要在生产中调用 getError
  • 保持纹理尺寸尽可能小;使用纹理贴图和批纹理
  • 将尽可能多的计算从浏览器转移到 GPU,这样速度会大大提高
  • 确保片段着色器得到优化,因为它是最常用的
  • 使用 requestanimationframe

更多资源可在附录 D 中找到。

捏造的例子

我们往往会忘记(或不完全理解)我们所学的东西,除非我们深入研究并亲自尝试。因此,我们现在将编造一个例子,让许多对象在场景中随机移动,以使 WebGL 足够慢,以注意到优化的改进。我们将增加对象的数量,直到我们获得一个差的帧速率,然后我们将使用我们已经获得的调试和性能知识来优化它。

我已经创建了一个例子,09/spheres_original.html,它使用了六个纹理(太阳、地球、月亮、火星、木星和土星)以及一个基本的光照模型和随机运动。最初,每个球形对象将有一个单独的绘制调用,并使用非交错数据。通过调整渲染对象的数量,我们可以降低帧率。在我的机器上,6 个对象以 60 fps 运行,50 个以大约 35 fps 运行,100 个以大约 30 fps 运行,这还可以。一千个对象将帧速率降低到大约 4 fps。我尝试了 10,000,但是我的浏览器只是挂了一会儿。你可以在图 9-22 左边看到 50 个物体,右边看到 1000 个。您可以通过更改以下行来调整在您的计算机上渲染的对象的数量,并且可以通过使用显示在画布左上角的 stats.js 小部件来观察帧速率:

9781430239963_Fig09-22.jpg

图 9-22 。左边五十个物体;右边 1000

var num _ spheres = 1000

较低的-4 fps 是一个很好的起点,可以看到性能的改善。打开 WebGL Inspector,我们可以捕捉到一帧,在 Trace 选项卡中看到每帧执行了 18000 多行;以及如图图 9-23 所示的 1000 次总抽取呼叫;“缓冲区”选项卡中有 4,000 个独立的缓冲区;并且使用了超过 40 MB 的缓冲数据。

9781430239963_Fig09-23.jpg

图 9-23 。跟踪捕获显示超过 18,000 条线路,并突出显示冗余调用

捕捉一帧并使用跟踪来为我们识别冗余,我们可以看到正在设置视口,并且每帧都在重新计算透视矩阵。我们的视角不会改变,我们的相机也不会移动,所以这是一种浪费。我们可以将下面几行移动到渲染循环之前:

gl.viewport(0,0,canvas.width,canvas . height);

mat4.perspective(45,canvas.width / canvas.height,0.1,100.0,p matrix);

gl .统一矩阵 4 Fv(glprogram . pmatrixuniiform,false,pmatrix);

这个改变稍微提高了 fps。我们将通过跟踪工作,直到我们尽可能多地消除冗余。接下来,我们可以看到,每次渲染对象时,我们都在重新启用顶点数组属性。我们也可以将这些线移动到渲染循环之前。

vertexpositionattributes = GL . getaftlocation(glprogram," warning explication ");

vertxnormalattribel = GL . getattriblocation(glprogram," warning xnormal ");

vertxtxcoordattributel = GL . getattriblocation(glprogram," avertextxcoord ");

GL . enableverticalscribarray(vertexpositionattributes);

GL . enablevertexattribarray(vertexNormalAttribute);

GL . enablevertxattribarray(vertxtxcoordattribute);

令人惊讶的是,这段代码将帧速率一直提高到了 50 fps!跟踪中不再标记冗余,调用总数从 18,000 减少到 11,000。现在让我们看看我们可以推动这个应用显示多少个对象。增加渲染对象的数量,直到帧速率降低到 15 以下。去除冗余的完整代码在 09/optimized _ 1 _ removed _ redundances . html 文件中,输出如图图 9-24 所示。

9781430239963_Fig09-24.jpg

图 9-24 。每秒 16 帧的三千个物体

image 注意:使用太多的缓冲液可能会导致错误的结果。对于 100,000 个对象,我获得了很高的帧速率,但是在 WebGL 检查器中查看后,只有 12,000 个 vbo,而不是应该有的 400,000 个。痕迹中也只有 33,000 行;我们需要至少 100,000 英镑来分别跟注。我们实际上没有 100,000 个对象的最大证据是 3,000 个对象的结果看起来与 100,000 个相同。

稍后在交错我的顶点属性后,我可以看到 20,000 个 vbo 和更完整的图像,如图 9-25 所示。

除了对 VBO 总数的限制之外,对每个 VBO 的元素数量也有限制。最大索引数是 2¹⁶ = 66536。

优化时,如果一个结果看起来好得不像真的,用你的直觉判断是否达到了浏览器的上限,或者结果是否被缓存在某个地方。

9781430239963_Fig09-25.jpg

图 9-25 。交错阵列出错。这些物体不是很圆

我们的下一个优化是将顶点位置、纹理坐标和法向数组交织成一个数组,这样我们就可以绘制更多的元素,因为我们为每个对象使用的缓冲区数量从 4 个减少到了 2 个(1 个缓冲区用于索引数组)。交错数组很好地清理了 drawScene 函数。清单 9-7 显示了我们使用交织数据的代码(生成的数据没有显示,但可以在 09/optimized _ 2 _ interleaved . html 文件中查看)并将其发送到 GPU。

清单 9-7。 交错数组属性指向

函数 drawScene()

{

for(var I = 0;I

setMvMatrix(球体位置[i])

setMatrixUniforms();

var active _ num = I % textures . length;

gl.activeTexture(gl)。纹理 0 + active_num:

GL . uniform 1 I(GL program . sampleruniform,active _ num);

bindBuffer(gl。ARRAY_BUFFER,triangles interleavedbuffers[I]);

gl . verticalpointer(vertexplicationattributes,3,GL)。浮点型,false,

8 * float 32 阵列。BYTES_PER_ELEMENT,0);

gl . verticalpointer(vertxnormplatform 属性,3,GL)。浮点型,false,

8 * float 32 阵列。每元素字节数,

3 *浮动 32 阵列。字节每元素);

gl . vertxtexcoordattribute,2,GL。浮点型,false,

8 * float 32 阵列。每元素字节数,

6 * float 32 阵列。字节每元素);

gl.drawElements(gl)。三角形,vertxinindexbuffer[I]。numItems(项目编号),

gl。UNSIGNED_SHORT,0);

}

}

现在,如果你输入了错误的步幅,比如 8 *而不是 8 * Float32Array。BYTES_PER_ELEMENT(总字节数)在前面的代码中,你会得到意想不到的结果,如图图 9-25 所示。

然而,使用正确的步幅值会产生如图 9-26 所示的预期结果。

9781430239963_Fig09-26.jpg

图 9-26 。交错阵列走向正确;10,000 个对象,但帧率较低

接下来,我们将把六个 256 x 256 的纹理合并成一个 512 x 512 的纹理图谱。你会记得纹理图谱图像之前在图 9-20 中显示过。使用纹理贴图集意味着我们不需要改变每个物体的活动纹理,我们永远不需要改变统一的采样值!

使用纹理地图最困难的部分是生成坐标。这对于每个内部图像的维度也是 2 的幂的纹理图谱来说实际上是相当容易的,尽管宽度和高度不必相等。我们跟踪每个图像的 x 和 y 偏移,以及图像长度相对于整体纹理的比例(从 0 到 1)图集尺寸,如下所示:

//x_offset,y_offset,x_scale,y_scale

var textureAtlasAreas = [

[0.0,0.0,0.5,0.5],//月亮

[0.5,0.0,0.5,0.5],//孙

[0.0,0.5,0.25,0.25],//土星

[0.0,0.75,0.25,0.25],//木星

[0.5,0.5,0.25,0.25],//地球

[0.5,0.75,0.25,0.25]//火星

];

然后,当我们设置我们的球体数据时,我们可以像这样访问这些信息:

var num _ textures = textureatlasareas . length;

for(var I = 0;I

var active _ num = i % num _ textures

var tex _ start _ x = texture atlasareas[active _ num][0],

tex _ start _ y = texture atlasareas[active _ num][1],

tex _ scale _ x = texture atlasareas[active _ num][2],

tex _ scale _ y = texture atlasareas[active _ num][3];

//纹理坐标

interleaveddata . push(u * tex _ scale _ x+tex _ start _ x);

interleaveddata . push(v * tex _ scale _ y+tex _ start _ y);

}

image 注意纹理图谱的一个潜在缺点是纹理边界可能会有颜色渗色。

我们将通过调用 gl.generateMipmap(gl。质感 _ 2D);我们要做的最后一个优化,也是最重要的一个优化是批量抽取调用。

当我们在示例的 setupSphereData 方法中生成网格,然后在 drawScene 方法中使用批的数量时,我们将执行批和每批球体的双循环,而不是循环通过我们要绘制的所有球体。

然而,我们现在至少面临一个问题。之前,我们更改了每个球体对象的模型视图矩阵。然而,现在我们将几个对象绘制批处理在一起,并且仍然每批只调整一次模型视图矩阵。这意味着一批中的每个球体都将在同一位置绘制。我们将只看到每批中最大的球体,较小的球体将隐藏在其中。例如,一次将 10,000 个球体批处理为 40 个,我们将渲染所有 10,000 个球体,但只看到 250 个。我们需要能够设置每个球体的模型视图。我们也不想不必要地更新制服。我们可以将这种计算转移到 GPU,而不是在 JavaScript 中为每个对象计算模型视图。这实际上是一种性能提升,因为 GPU 速度快得多。我们还必须在每次绘制时更新一次平移和旋转量的统一值,而不是针对每个对象。我们计算每个对象模型视图矩阵的原始 JavaScript 代码如清单 9-8 所示。

清单 9-8。 JavaScript 代码用于计算每个对象模型的视图矩阵值

函数集矩阵

{

mat 4 . identity(mv matrix);

mat4.identity(法线矩阵);

mat4.translate(mvMatrix,[sp.x_offset,sp.y_offset,sp . z _ offset]);

mat4.rotate(mvMatrix,sp.angle,[sp.x_angle,sp.y_angle,sp . z _ angle]);

mat4 .反向(mvMatrix,法向矩阵);

sp . x _ angle+= math . random();

sp . y _ angle+= math . random();

sp . z _ angle+= math . random();

sp . x _ offset =(math . cos(sp . angle)* sp . x _ offset _ orig);

sp . y _ offset =(math . sin(sp . angle)* sp . y _ offset _ orig);

sp . z _ offset =-25.0+12.0 * math . sin(sp . angle);

sp . angle+= 0.005;

}

我们可以创建统一的变量来存储这些值,并在我们的顶点着色器中使用它们,而不是重新计算在所有球体中保持不变的余弦和正弦值:

均匀浮动 uCosTime

统一浮动使用时间;

我们现在也将只计算一次球体几何图形,并将其存储,如清单 9-9 所示。

清单 9-9。 计算单位球面上的点

var unit _ sphere = null

函数 calculateUnitSpherePoints(纬度带,经度带)

{

//O(n²)三角运算-代价高昂!

unit_sphere = {

【顶点】:[],

"紫外线":[]

};

的(订单编号= 0;纬度,经度++)

var theta = latNumber * Math。PI/latitude bands;

var sinet = math . sin(theta):

var cosTheta = math . cos(theta);

for(var long number = 0;longNumber < = longitudeBandslongNumber++) {

var phi = longNumber * 2 * Math。PI/longitude bands;

var sinphi = math . sin(phi);

var cos phi = math . cos(phi);

var x = cosPhi * sinTheta

var y = cosTheta

var z = sinPhi *语法;

var u = 1-(long number/longitude bands);

其中 v =分数字/纬度带;

//位置

unit _ sphere . vertices . push({ " x ":x," y": y," z "😒 });

//纹理坐标

unit_sphere.uvs.push({"u": u," v ":v });

}

}

}

我们可以使用存储的坐标来生成场景中的所有其他球体:

//位置

interleaveddata . push(radius * vertex . x+spherePositions[mesh _ number])。x _ offset _ orig);

interleaveddata . push(radius * vertex . y+spherePositions[mesh _ number])。y _ offset _ orig);

interleaveddata . push(radius * vertex . z+spherePositions[mesh _ number])。z _ offset _ orig);

//正常

interleaveddata . push(vertex . x);

interleaveddata . push(vertex . y);

interleaveddata . push(vertex . z);

//纹理坐标

interleaveddata . push(uv . u * tex _ scale _ x+tex _ start _ x);

interleaveddata . push(uv . v * tex _ scale _ y+tex _ start _ y);

这允许我们不改变 MVP 矩阵,也不需要在生成后使用 spherePositions 数组数据。我们现在可以用清单 9-10 中的代码批量绘制我们的对象。

清单 9-10。 批量绘制我们的对象

var num _ spheres = 15000

var num _ per _ batch = 250

var batches = num _ spheres/num _ per _ batch;

函数 drawScene()

{

GL . uniform 1 f(GL program . costime uniform,math . cos(current time));

GL . uniform 1f(GL program . sin time uniform,math . sin(current time));

for(var I = 0;I

bindBuffer(gl。ARRAY_BUFFER,triangles interleavedbuffers[I]);

gl . verticalpointer(vertexplicationattributes,3,GL)。浮点型,false,

8 * float 32 阵列。BYTES_PER_ELEMENT,0);

gl . verticalpointer(vertxnormplatform 属性,3,GL)。浮点型,false,

8 * float 32 阵列。每元素字节数,

3 *浮动 32 阵列。字节每元素);

gl . vertxtexcoordattribute,2,GL。浮点型,false,

8 * float 32 阵列。每元素字节数,

6 * float 32 阵列。字节每元素);

gl.drawElements(gl)。三角形,vertxinindexbuffer[I]。numItems(项目编号),

gl。UNSIGNED_SHORT,0);

}

current time+= 0.01;

}

由于我们受限于单个 VBO 可以有多大,我们将使用 10 个分区的球体(每个球体 600 个索引)而不是 30 个分区(每个球体 5400 个索引)来演示批处理的加速。一次渲染 15,000 个这样的球体会产生 3 fps 的效果。渲染 15,000 但一次批处理 250 会产生更好的 45 fps,如图 9-27 所示。

9781430239963_Fig09-27.jpg

图 9-27 。左图:批量大小为 1 以 3 fps 渲染;右图:-批量大小为 250 以 47 fps 渲染

摘要

本章讨论了如何调试 WebGL 应用并提高性能。这是两个重要的主题,将有利于您的 WebGL 开发和用户对您的应用的享受。一个以 3 fps 爬行的复杂场景和一个以 40 fps 移动的场景之间的差别是显著的,这可能是用户喜欢你的应用还是放弃它的差别。WebGL 可能很难调试,因为有许多因素在起作用:特定的浏览器、计算机和使用的 GPUJavaScript API 着色器程序;以及纹理等资源。其中每一个都可能是误差的来源。幸运的是,有强大的工具可以帮助我们,从我们使用的 IDE 到浏览器开发工具,以及使用 WebGL inspector。

在下一章,也是最后一章,我们将介绍各种各样的效果、技巧和窍门——图像处理、非真实感着色器,以及使用 framebuffer 对象来确定我们场景中的哪个元素当前被鼠标选中并实现阴影贴图。

十、效果、提示和技巧

在本章中,我们将介绍各种 WebGL 效果、提示和技巧,例如:

  • 基本图像处理
  • 使用卷积滤波器的图像处理
  • 反混淆
  • 非真实感着色器
  • 帧缓冲和渲染缓冲
  • 从画布中拾取对象
  • 阴影贴图实现

效果

通过图像处理和卷积滤波器可以实现各种各样的效果,如锐化、模糊、灰度、棕褐色调、颜色调整和边缘检测。

为了应用这些效果,我们将从加载纹理图像开始。然后,我们将在片段着色器中改变纹理中每个像素的原始颜色值。对于这些例子,设置类似于第六章中的一些例子,其中算法被用于纯粹在片段着色器中创建图像。这一次,我们有一个开始改变纹理图像。实际上,纹理图像可能来自 HTMLVideoElement 对象,因此我们可以使用这些相同的技术动态地改变视频流。我们将专注于静态图像处理。

基本图像操作

我们的第一个图像处理示例 将显示灰度、反转颜色值和原始纹理图像旁边的绿色图像。为此,我们首先设置一些效果常数和一个变量来存储一个统一的值,该值将通知我们的着色器使用哪个效果:

var NO_EFFECT = 0,

灰度 _ 效果= 1,

负 _ 效= 2,

GREEN _ TINT _ EFFECT = 3;

var effectUniform = null;

当我们渲染到画布上时,我们将实际绘制场景四次,每次使用四分之一的视口并改变效果。效果图如图图 10-1 所示。不幸的是,很难看出黑白印刷有什么不同,所以请访问网站【http://www.beginningwebgl.com/ 获得全彩色版本。

9781430239963_Fig10-01.jpg

图 10-1 。左上:原图;右上:灰度;右下:反转颜色;左下角:颜色更绿

调整视口和重新渲染可以让我们很容易地一次看到几个变化,这是在同一个场景中使用多个视口的应用,如第一章中所讨论的。视窗设置的代码如清单 10-1 所示。其中,我们在视口的不同区域绘制了四次,并通过更改统一值通知片段着色器每次应用哪个效果。

清单 10-1 。 为视口设置的代码

。。。

//左上角

gl.uniform1i(effectUniform,NO _ EFFECT);

gl.viewport(0,canvas.height/2.0, canvas.height/2.0, canvas . height/2.0);

draw scene();

//左下角

gl.uniform1i(effectUniform,GREEN _ TINT _ EFFECT);

gl.viewport(0,0,canvas.height/2.0,画布.高度/2.0);

draw scene();

//右上角

gl.uniform1i(effectUniform,gray _ EFFECT);

gl.viewport(canvas.height/2.0,0,canvas.height/2.0, canvas . height/2.0);

draw scene();

//右下角

gl.uniform1i(effectUniform,NEGATIVE _ EFFECT);

gl.viewport(canvas.height/2.0,canvas.height/2.0, canvas.height/2.0, canvas . height/2.0);

draw scene();

。。。

完整的代码可以在 10/01_image_processing.html 文件中找到。顶点着色器非常简单,只将原始的 x 和 y 坐标传递给片段着色器:

十一、后记

image

WebGL 的未来

WebGL 的未来会怎样?在这篇后记中,我们将讨论 WebGL 的优点和一些问题,并对它的未来进行推测。

为了让 WebGL 有一个光明的未来,而不是像其他过去的 3D 浏览器尝试(如虚拟现实标记语言(VRML))一样失败,它需要以下几点:

  • 支持
  • 来自开发社区的采用,尤其是游戏开发者
  • 改进和积极发展

支持

这里我们将看看来自浏览器和设备的支持。

浏览器支持

正如书简介中提到的,Chrome 和 Firefox 在支持 WebGL 方面做得非常好。Safari 和 Opera 正在改进,IE 近期没有原生支持 WebGL 的计划。虽然五年前这可能是一场灾难,但 IE 并没有获得它曾经享有的市场份额——Chrome 已经超过了它,Firefox 也紧随其后。

移动设备支持

目前支持 WebGL 的移动设备数量很少,但随着每一款新设备的发布,这一水平将会提高,到 2013 年,这一水平将会大大提高。

目前,有几种支持 WebGL 的移动浏览器:Firefox Mobile、Android 浏览器、Opera Mobile(仅限 Android)、BlackBerry Playbook 和 iOS Mobile Safari(目前仅支持 iAd)。

移动市场份额正在增长,是一个重要的发展领域。由于 Adobe 最近宣布将停止对移动 Flash 的支持,WebGL 有了一个更好的机会来确立自己作为移动 3D 首选技术的地位。

Florian Boesch 的网站webglstats.com/有一些非常有趣的统计数据,关于跨浏览器、设备和操作系统的各种 WebGL 指标的当前支持。

收养

正如本书中提到的,Google 已经将 WebGL 用于其身体、地图和地球应用。

我们在第九章中展示了 Firefox 正在使用 WebGL 对文档对象模型(DOM)进行新的 3D 调试可视化。获得知名公司的支持和使用是很重要的,谷歌、Mozilla、苹果和 Opera 都提供了支持。同样重要的是,要有写得好的框架来降低 3D 编码的门槛。Three.js 这样的框架已经很容易使用了,并且会继续变得更好。

WebGL 有何优势

  • 不需要插件。

  • The timing is right. 3D in the browser is more useful now than back when VRML tried. GPUs are more powerful. WebGL is part of the larger movement of HTML5 and related technologies, which adds many browser enhancements which are making it possible to create applications previously only possible on the desktop.

    “几十年来,网络一直在通过吸管吸取这种能力,但有了 WebGL,当谈到图形处理能力时,就好像吸管被消防水管取代了一样。。."www . tnl . net/blog/2011/10/23/web GL-and-the-future-of-the-web/

  • Web 应用没有平台兼容性问题,也不需要安装。

  • WebGL 框架使得开始使用 WebGL 变得越来越容易。

  • 对于有经验的图形程序员来说,在底层调整 WebGL 的能力是非常有用的。

  • 许多令人敬畏的演示。

  • 规范的透明开发。

  • 经验和现有开发人员。Khronos 还负责 OpenGL 和 Collada。有许多当前的 OpenGL 和 OpenGL ES 开发人员可以相当容易地学会/过渡到 WebGL API。

关系

WebGL 功能强大,非常有前途。然而,它是一种相对较新的语言,存在一些问题,包括:

  • 缺少微软的支持。如前所述,这并不像微软统治浏览器市场时那么重要。究竟是出于安全考虑还是出于对自身 DirectX 技术的兴趣而不支持 WebGL,只有微软能说得准。
  • 安全问题。 GPU 黑名单和安全性更高的新型显卡将有助于解决 GPU 问题。跨来源资源共享等其他网络安全措施将在保持安全性的同时实现灵活性。
  • Flash 或其他用于 3D 的技术。如上所述,Flash 停止移动支持有助于缓解这种担忧。
  • JavaScript 的性能问题。JavaScript 很慢。已经进行了改进,例如类型化数组,并且正在研究更多的优化。
  • 游戏开发者需要加入进来。现在将详述这一点。

游戏开发商

Florian Boesch 在 http://code flow . org/entries/2011/sep/11/webgl-and-html 5-challenges-for-the-future/的一篇出色的博客文章解释了 web GL 如何需要游戏开发者采用它。在条目中,列出了游戏开发人员需要的几个功能,其中与 WebGL 相关的功能特别多:多个渲染目标、几何实例化、顶点着色器中的纹理查找以及浮点纹理。目前浏览器对这些功能的支持可以在这里提到的 webglstats.com 链接中找到。

积极发展

WebGL 规范、未来扩展和浏览器实现都在积极开发中。

扩展

WebGL 语言的核心扩展正在开发中,可以在 http://www.khronos.org/registry/webgl/extensions/观看。在目前列出的扩展中,有三个特别有用:

  • 各向异性过滤,提高以倾斜角度观看的纹理质量
  • 阴影贴图的深度纹理
  • 压缩纹理

可能很快添加的未来功能包括:

  • 更多扩展,如跨上下文共享或多个呈现目标。

  • web workers 中的多线程。这将允许上传纹理和数据,而不会阻塞主线程。

  • 异步上下文创建和上下文之间的资源共享。

最后一句话

技术领域没有什么是确定的,但我坚信 WebGL 会一直存在下去——否则我不会花时间和精力写这本书。WebGL 是一项非常有前途的技术,它的开发恰逢其时,因为浏览器正处于快速发布周期,并且每天都在支持更多的高级功能。这也是一个越来越多功能强大的计算机被塞进任何地方的移动设备的时代。WebGL 已经是一项非常有用的技术。框架改进将有助于降低新开发人员的门槛,更多的调试和实用工具将被创建,性能将继续提高。

十二、附录 A:基本 HTML5 和 JavaScript

有许多改进和特性是 HTML5 的一部分或与之相关:地理定位、新的输入类型、表单验证、本地存储和 web 套接字(仅举几例)。涵盖所有新的内容既不可行,也不值得在这里讨论——最近有大量书籍深入探讨了这一问题。

基本 HTML5

虽然您不需要了解 HTML5 的所有新的和伟大的内容,但为了最大限度地加深您对代码示例的理解,我们将展示您需要了解的与 HTML 4 的相关差异。

简洁

首先,HTML5 通过标准化开始标记和对脚本和样式的简化,允许更紧凑地编写文档。在 HTML 4 中,你可能会有类似下面的代码列出 A-1 :

清单 A-1 。 一个极简的 HTML 4 文档

<title>例子</title>

十三、附录 B:图形刷新程序

本书假设读者对 3D 图形有一个基本的了解,但是我们将在本附录中刷新一些相关主题的记忆。

像素

当我们在计算机屏幕上以数字方式呈现一幅图像时,它是由一个矩形网格组成的,这个网格由称为像素的单个颜色点组成。这种类型的表示被称为光栅图形位图 。显示图像的真实程度取决于屏幕上的像素数量:分辨率。在图 B-1 中,我们在左边显示了一个输入图像,在中间显示了一个 4 × 4 像素的网格表示,在右边显示了一个 16 × 16 的网格表示。随着分辨率的增加,原始图像与渲染图像之间的差异会减小。

9781430239963_AppB-01.jpg

图 B-1 。左图:输入图像;中心:4 × 4 像素输出;右图:16 × 16 像素输出

基元

图形元素是我们可以用来组成图像和场景的最小构建模块。我们可用的图元取决于所使用的语言,可以是点、线、多边形(如三角形和四边形)或一些高级语言中的立体形状。

彩色

颜色有几个属性,包括色调(色调)、饱和度(暗度)和值(强度)。事实上,颜色可以由色调-饱和度-值(HSV)颜色模型中的这三个属性来表示。然而,呈现颜色的方式不止一种,这取决于我们使用的是加色理论还是减色理论,以及诸如打印图像或在屏幕上显示图像等应用用途。

当我们打印图像时,通常使用减色法 CMYK 模式,它有四个通道,由青色、洋红色、黄色和暗度(K)组成。这就是为什么有些打印机有彩色 CMY 墨盒和黑色墨盒。

在计算机显示器上,颜色值通常使用加色 RGBA 方案来表示,该方案有四个通道,包括红色、绿色、蓝色和 Alpha(透明度)值。每个通道值的范围可以是 0.0 至 1.0 的浮点值、0 至 255 的整数值或 0×000000 至 0×ffffff 的十六进制值。

为了从 CMY 转换到 RGB,我们采用[(1.0,1.0,1.0)-CMY]。所以黄色在 CMY 是(0.0,0.0,1.0),在 RGB 是(1.0,1.0,0.0)。在本书中,我们将专门使用 RGB(A)颜色模型。

image 更多关于 RGBA 彩色格式的信息可以在维基百科上的en.wikipedia.org/wiki/RGBA_color_space找到。

坐标系统

笛卡尔坐标系 以数学家、哲学家和作家勒内·笛卡尔的名字命名,在二维中使用(x,y)对,在三维中使用(x,y,z)三元组。原点是所有轴的交点。在二维中,这是(0,0),在三维中是(0,0,0)。对于每个轴,原点一侧的值增加,另一侧的值减少。有两个独立的三维坐标系方向,如图图 B-2 所示。它们之间的区别是相对于 x 和 y 轴的 z 方向。

9781430239963_AppB-02.jpg

图 B-2 。两种不同的 3D 坐标系方向

变换

初等变换或仿射变换会改变图形的顶点。有三种基本变换:平移、旋转和缩放,如图图 B-3 所示。

9781430239963_AppB-03.jpg

图 B-3 。平移(左)、旋转(中)和缩放(右)的变换

图 B-4 中显示了向右移动 3 个位置和向上移动 2 个位置的着色区域的平移。

9781430239963_AppB-04.jpg

图 B-4 。翻译一张图片

图像子区域围绕其中心像素顺时针旋转 90 度,如图 B-5 所示。

9781430239963_AppB-05.jpg

图 B-5 。图像的旋转

在图 B-6 中显示了两倍于已着色子区域原始尺寸的缩放比例。

9781430239963_AppB-06.jpg

图 B-6 。图像的缩放

图形编程使用了大量的数学知识,尽管库可以抽象掉大量的计算,但是了解一些基本知识还是很有好处的。

数学

我们首先应该知道的是角度、度数、圆周率和弧度。

角度

两条光线相交形成一个角度,如图 B-7 左侧所示。从技术上来说,角度是两条射线的弧长与圆内接半径的商的度量,如图 B-7 右侧所示。圆有 360 度,所以角度有时用度来度量。

9781430239963_AppB-07.jpg

图 B-7 。左图:形成内角的两条射线;右:圆内的角度

圆周率

用π表示的常数π大约是 3.14159,它是圆的周长与直径的比值。圆周率广泛应用于三角学、几何学和其他数学分支。

弧度

除了度,我们还有弧度,它被定义为 360 度= 2π弧度。这意味着 1 弧度约为 57.3 度。图 B-8 ,显示了各种角度和四个象限直角的弧度值。角度 A 看起来大约是 45 度,E 大约是 150 度,这使得角度 B 大约是 30 度。角度 D 看起来是-60 度,这将使角度 C 大约为 30 度。

9781430239963_AppB-08.jpg

图 B-8 。各种旋转角度

三角形的角和边的关系是在称为三角学的数学分支中研究的。

三角学

对于直角三角形(一个角正好是 90 度)和三角形中的另一个角 q,我们可以知道边长的比值。图 B-9 显示了直角的斜边(直角的对边)、对边和邻边。

9781430239963_AppB-09.jpg

图 B-9 。直角三角形的边

给定这些边和角度 q,我们可以用边来表示角度如下:

sin θ =对边/斜边

cos θ =邻边/斜边

tan θ =相对/相邻

这些关系常被记忆为 soh、cah、toa ,它们是关系名和边的首字母缩写。

旋转

在二维中,旋转使用旋转矩阵:

[什么-Sina][x]=[xcosa-ysina]

[sinA 什么] [y] = [xsinA + ycosA]

我们可以用这些方程来计算旋转 A 度后新的 x,y 坐标。

矢量

有了坐标为(x 1 ,y 1 ,z 1 ),(x 2 ,y 2 ,z 2 的两个点,我们现在将定义一些有用的计算。

点积

点积通过返回两个输入向量按分量相乘的和来返回标量值:

x1* x2+y1* y2+y1* z2

交叉乘积

叉积 (x,y,z)返回一个垂直于由两个输入向量形成的平面的向量。因此,我们用它来寻找法向量。叉积的计算方法如下:

x = y1z2–y2 z1

y =-x1* z2+x2* z1

z = x1* y2-x2* y1

长度

两点之间的长度可以计算为每个分量差的平方和的平方根:

平方根((x1-x2)2+(y1-y2)2+(z1-z2)2)

十四、附录 C:WebGL 规格和零碎事项

本附录包含我们提到过但未完全涵盖的规范的某些部分,在此列出以供参考。

webcl 上下文属性

当我们获得我们的 WebGL 上下文时,我们可以有选择地向它传递一个包含以下部分或全部属性的对象:

字典 WebGLContextAttributes {

布尔 alpha =真;

布尔深度= true

布尔模具=假;

boolean 抗锯齿= true

boolean premultiplied alpha = true;

boolean preserveDrawingBuffer = false;

};

我们展示了如何在一个第五章的投射实例中保存绘图缓冲区:

GL = canvas . get context(" webgl " { preserve drawing buffer:true })| |)

canvas . get context(" experimental-web GL ",{ preserveDrawingBuffer:true });

通过保留缓冲区内容而不是自动交换缓冲区,我们可以看到物体运动的轨迹,还可以产生运动模糊等效果。如果性能是关键,并且我们不需要 alpha 或深度测试,我们可以禁用这些属性。如果我们需要模板缓冲区,我们可以启用它。我们在第十章中展示了如何禁用抗锯齿。premultipliedAlpha 值影响画布的 Alpha 组件如何影响图像的整体颜色。将该值设置为 false 会使画布元素的 WebGL 颜色计算与 2D 上下文相同。

纹理属性

在第三章中,我们忽略了一些纹理选项。

立方体贴图目标

对于立方体贴图纹理,目标属性可以是以下之一:

纹理立方体贴图,纹理绑定立方体贴图,

纹理 _ 立方体 _ 贴图 _ 正 X,纹理 _ 立方体 _ 贴图 _ 负 X,

纹理 _ 立方体 _ 贴图 _ 正 _Y,纹理 _ 立方体 _ 贴图 _ 负 _Y,

纹理 _ 立方体 _ 贴图 _ 正 Z,纹理 _ 立方体 _ 贴图 _ 负 Z,

最大立方体贴图纹理大小

泰晤士河 2D

纹理的格式如下:

阿尔法:阿尔法

RGB: R、g、b 颜色

RGBA:红、绿、蓝颜色和阿尔法

亮度:亮度

亮度 _ALPHA:亮度,ALPHA

这些类型可以是:

无符号字节

无符号 _ 短整型 _4_4_4_4

UNSIGNED _ SHORT _ 5 _ 5 _ 5

无符号 _SHORT_5_6_5

以下组合是合法的:

无符号字节/ RGBA,RGB,亮度,亮度阿尔法

UNSIGNED_SHORT_4_4_4_4 / RGBA

UNSIGNED_SHORT_5_5_5_1 / RGBA

UNSIGNED_SHORT_5_6_5 / RGB

帧缓冲区和渲染缓冲区目标和附件

在第十章中,我们介绍了帧缓冲区和渲染缓冲区 。其他合法附件/格式组合如下:

深度 _ 附件/深度 _ 组件 _16

模板 _ 附件/模板 _ 索引 8

深度 _ 模板 _ 附件/深度 _ 模板

颜色附件:COLOR_ATTACHMENT0

附加格式:RGBA、RGBA4、RGB5_A1、RGB565、STENCIL_INDEX

以下并发附件组合是非法的:

深度 _ 附件/深度 _ 模板 _ 附件

模板 _ 附件/深度 _ 模板 _ 附件

深度 _ 附件/模板 _ 附件

十五、附录 D:额外资源

WebGL 是一个新兴的技术,有很多方面。我尽了最大努力在本附录中汇编了良好的补充学习资源。

伙伴网站

您可以在以下地址找到本书的配套网站 :

开始 WebGL

www.beginningwebgl.com/

github page(github 页)

github . com/bdancilla/begin ningtwebgl

由于网络的不稳定性,很快会产生死链、过时的资源或涌现的新资源,请参考配套网站,了解本附录中列出的资源的最新版本。

主题

这里列出了本书中提到的许多技术的更多资源(按字母顺序排列)。

Ajax

XMLHttpRequest 规范

www.w3.org/TR/XMLHttpRequest/

Mozilla XMLHttpRequest 页面

developer . Mozilla . org/En/XMLHttpRequest/Using _ XMLHttpRequest

调试

Khronos 调试 wiki 页

www.khronos.org/webgl/wiki/Debugging

WebGL 检查器

benvanik . github . com/webgl-inspector/

我们给了

尖端的 Chrome WebGL 实验

www.chromeexperiments.com/webgl

Khronos 演示资源库

www.khronos.org/webgl/wiki/Demo_Repository

不错的水演示

madebyevan.com/webgl-water/

HTML

HTML 5 和 4 的区别

www . w3 . org/tr/html 5-diff/

画布元素

www.w3.org/TR/html5/the-canvas-element.html

JavaScript〔??〕〔??〕

道格拉斯·克洛克福特网站

javascript.crockford.com/

框架

jquery.com/

数据

www.json.org/

灯,MAMP ,WAMPT3

马姆普

www.mamp.info/en/index.html

XAMPP(洗发精)

www.apachefriends.org/en/index.html

EasyPHP

www.easyphp.org/

比特南

bitnami.org/

欧佩克

sourceforge.net/projects/opew/

浏览器设置调整

github . com/mrdoob/three . js/wiki/How-to-run-things-locally

库和框架

框架列表

www . khronos . org/webgl/wiki/user _ contributions

GLGE

项目页面

www.glge.org

辅导的

www . rozengain . com/blog/2010/06/23/hands-on-web GL-basic-glge-tutorial/

菲洛勒

项目页面

www.senchalabs.org/philogl/

资源

www . slide share . net/Philo GB/leaving-flatland-getting-started-with-web GL-sxsw-2012

三个。JS

项目页面

mrdoob . github . com/3 . js/

文件

mrdoob.github.com/three.js/docs/latest/

维基网

github . com/mrdoob/3 . js/wiki

学习资源:Paul Lewis

aerotwist.com/tutorials/

学习资源:Jerome Etienne

learninthreejs . com/

整体物体的漂亮图表

ushiroad.com/3j/

www.12devsofxmas.co.uk/2012/01/webgl-and-three-js/

照明

直接照明模型

www . lighthouse 3d . com/tutorials/glsl-tutorial/directional-lights-ii/

www.ozone3d.net/tutorials/glsl_lighting_phong_p3.php

Phong 反射模型

en.wikipedia.org/wiki/Phong_reflection_model

图 3-13 是 http://en . Wikipedia . org/wiki/File:Phong _ components _ version _ 4 . png 的变体,它在 GNU 自由文档许可证下获得许可

全局照明模型

http . developer . NVIDIA . com/gpugems 2/gpugems 2 _ chapter 38 . html

环境遮挡

http . download . NVIDIA . com/developer/GPU _ Gems _ 2/GPU _ Gems 2 _ ch14 . pdf

en.wikipedia.org/wiki/Screen_Space_Ambient_Occlusion

www . game rendering . com/category/lighting/ssao-lighting/

反射和折射

http . developer . NVIDIA . com/gpugems 3/gpugems 3 _ ch17 . html

http . developer . NVIDIA . com/gpugems 2/gpugems 2 _ chapter 19 . html

阴影映射

fabiensangard . net/shadow mapping/index . PHP

数学

Wolfram Mathworld(黑钨矿)

mathworld.wolfram.com

分形

users.erols.com/ziring/mandel.html

66.39.71.195/Derbyshire/manguide.html

davis.wpi.edu/∼matt/courses/fractals/index.htm

www.fractalforums.com/

矩阵和向量库

gl-matrix.js

github.com/toji/gl 矩阵

西尔威斯特

sylvester.jcoglan.com/

webgl mj

code . Google . com/p/webgl-mj/

基准

stepheneb . github . com/web GL-matrix-benchmarks/matrix _ benchmark . html

网格文件格式

波前(obj)格式

en . Wikipedia . org/wiki/wave front _ obj

格式化整理

en . Wikipedia . org/wiki/collada

Three.js 内部 JSON 格式

github . com/mrdoob/three . js/wiki/JSON-Model-format-3.0

性能和最佳实践

Mozilla 开发者网络最佳实践

developer.mozilla.org/en/WebGL/WebGL_best_practices

Gregg Tavares 谷歌 I/O 2011

www . YouTube . com/watch?v=rfQ8rKGTVlg

games . greggman . com/game/web GL-techniques-and-performance/

static . Google user content . com/external _ content/untrusted _ dlcp/www . Google . com/en//events/io/2011/static/notes files/webgltechnizandperformancenotes . pdf

使用关于进行分析:跟踪

www.html5rocks.com/en/tutorials/games/abouttracing/

物理

学问

www.physicsclassroom.com

WebGL 演示

www.ibiblio.org/e-notes/webgl/gpu/contents.htm

Javascript 库:

2D 港口

code.google.com/p/box2dweb/

github . com/kri pke/box 2d . js

子弹港

github . com/kri pke/ammo . js/

大炮

github . com/text PPE/cannon . js

物理学家

chanderprall . github . com/physij/

教程

creative js . com/2011/09/box2d-JavaScript-tutorial-series-by-Seth-ladd/

learning three js . com/blog/2012/06/05/3d-physics-with-three-js-and-physijs/

www . html 5 gamedevs . com/2012/01/18/web GL-bullet-js-experiences-history-programming-slides/

WebGL

当前浏览器支持

caniuse . com/# search = webgl

Khronos 集团维基

www . khronos . org/webgl/wiki/main _ page

www . khronos . org/web GL/wiki/Tutorial # Creating _ the _ Shaders

WebGL 规范

www.khronos.org/registry/webgl/specs/latest/

学习 WebGL

learningwebgl.com/blog/

Mozilla 开发人员区域

developer . Mozilla . org/en/webgl

Opera 开发者区

dev . opera . com/articles/view/porting-3d-graphics-to-the-web-web GL-intro-part-2/

参考卡

www . khronos . org/files/web GL/web GL-reference-card-1 _ 0 . pdf

报告

www.khronos.org/webgl/wiki/Presentations

教程

www.html5rocks.com/en/features/graphics

混合

mrdoob . com/lab/JavaScript/web GL/blending/blend func . html

WebGL 未来

挑战和预测

www.irrlicht3d.org/pivot/entry.php?id=1255

code flow . org/entries/2011/sep/11/web GL-and-html 5-挑战未来/

www . tnl . net/blog/2011/10/23/web GL-and-the-future-of-the-web/

支持统计

www.riastats.com/

weblstat . com/

扩展注册表

www.khronos.org/registry/webgl/extensions/

WebGL SL (OpenGL 是 SL)

OpenGL ES 2.0 着色语言版本 1.0

www . khronos . org/registry/gles/specs/2.0/GLSL _ ES _ Specification _ 1 . 0 . 17 . pdf

提供 WebGL 快速参考卡

www . khronos . org/files/web GL/web GL-reference-card-1 _ 0 . pdf

在线 GLSL 编辑

weblplayground . net/

spidergl.org/meshade/

www . kickjs . org/example/shader _ editor/shader _ editor . html

现有着色器

code . Google . com/p/GL slang-library/source/browse/trunk/trunk/GL slang/shaders/material/

posted @   绝不原创的飞龙  阅读(173)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示