原文: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 有许多演示,包括:
www.chromeexperiments.com/webgl
code.google.com/p/webglsamples/
aleksandarrodic.com/p/jellyfish/
- 谷歌身体(现在的 http://www.zygotebody.com)、谷歌地图的一部分和谷歌地球
www.ro.me/tech/
alterequalia . com/
支持的环境
你的浏览器支持 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、可编程和不可编程的管道组件如何交互的高级视图。
图 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 所示。
图 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 加载它们
注意默认情况下,<脚本>标签将 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 的右边制作纹理和光照网格。
图 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 左边是源图像的坐标,右边是等效的纹理坐标。
图 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 中。
注意我们将图像存储在 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 设置如何处理纹理过滤和包装的选项。我们将在本章后面更详细地介绍这两个新功能。最后,我们检查我们的纹理对象是否有效,如果无效,就向控制台输出一条错误消息。
注意这只是加载图像数据的一种方式。您也可以在现有的< 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 所示。
图 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]);//第二个元素
注更多 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 对象的第二种方式让我们可以忽略不需要覆盖的参数,也不要求参数按照任何顺序排列。
注意本章中的代码并没有针对性能进行优化。因为我们只有几个网格,这无关紧要。然而,对于涉及许多绘制调用的更复杂的场景,我们将需要编写优化的代码。请参考第九章了解最佳实践和提高性能的方法。
球体
为了生成球体网格,函数 setupSphereMesh 如清单 4-3 所示。第一部分让我们设置缓冲指数,半径,平移,颜色,划分,以及是否使用平滑阴影。接下来,我们使用球坐标生成网格。当我们渲染一个球体时,它由水平线(如果地球被建模为球体,请将纬线想象为与赤道平行)和垂直线(请将它们想象为从北极到南极并代表时区)组成。纬线和经线相交的地方就是顶点。顶点向“极点”靠近,而向“赤道”远离细分越多,网格就越接近真实的球体。
注单位球面上每一点的法线值就是该点本身(缩放或平移前)。记住法向量是垂直指向表面的方向,从原点开始,这个方向就是向量本身。球坐标是单位长度的,所以这个向量已经被归一化了。
清单 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 中的所示。
图 4-2 。纬度和经度分别为 5、10 和 20 的球体
在清单 4-3 中,我们省略了平面阴影代码。在我们讨论了平滑阴影和平坦阴影的区别之后,我们将回到这个代码。
重新审视照明
照明是图形的核心,我们将在本章涵盖更多的灯光实现 细节,从阴影模型、传统的 Phong 照明模型开始,最后是全局辐射模型。
阴影模型
给多边形着色有两种基本方法:平滑着色和平滑着色。平面阴影 表示整个多边形是一种颜色。我们对所有顶点使用相同的法向量。因此,对于同一个顶点,边相交的法线可能不同,这取决于整个面的法线向量值。这种差异意味着相邻边上的照明值会有很大差异,因此您会看到一条边在哪里结束,另一条边在哪里开始。相反,平滑阴影 表示对颜色和法线值进行插值。这可以在顶点着色器(VS)中完成,如 Gouraud 着色,或在片段着色器(FS)中完成,如 Phong 着色。这两种着色技术将在本章后面详细介绍。
法向量再探
让我们先来看看多边形边相交且顶点共享时平面阴影的法向量是什么样子的(见图 4-3 )。
图 4-3 。平面着色:一种颜色,每个表面一个法线
正如你在图 4-3 中看到的,共享顶点处的法线是脱节的。相邻多边形的值之间会有明显的跳跃。使用平面着色时,如果入射的镜面反射光没有照射到顶点,镜面反射高光(回想一下镜面反射是在特定方向反射的光)将被忽略。因此,平面着色通常根本不计算镜面反射。
使用平滑着色时,共享顶点与其共享的所有面进行平均。图 4-4 的右边显示了如何使用一个新的法向量,它是两个共享边的平均值。当然,左图也有一些顶点不是跨多个三角形共享的,也有一些是由三个三角形共享的。
图 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 。
图 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 )。
图 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 显示了位移随时间变化的样本图,接着是速度随时间变化的曲线图,然后是加速度随时间变化的曲线图。例如,请注意,我们可以在减速时向前移动,也可以在零加速度时快速移动。
图 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]:
注意除了将鼠标处理程序附加到画布上,我们还可以将它们附加到整个文档上。这在前面的例子中很有用,因为移出画布将会停止鼠标事件的捕获,并在我们移回画布时产生意想不到的不良结果。鼠标按钮可能仍然是按下的,但是我们需要首先释放它,然后在事件被重新捕获之前再次单击并按住它。
通常最好先进行场景范围的变换,然后进行对象特定的变换。
使用鼠标滚轮控制缩放
滚动鼠标滚轮 常用于控制场景的放大和缩小。为此,我们将为 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);
});
…
注意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 左侧所示。现在让我们添加第一个碰撞检测案例来防止这种情况。
图 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 所示。
图 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 显示立方体不太适合球体,反之亦然。(这两件事我们都不想做。)
图 5-4 。左图:包围球中的立方体;右图:边界框中的球体
注关于球体和立方体几何图形的复习,请参考en.wikipedia.org/wiki/Sphere
和en.wikipedia.org/wiki/Cube
有了包围体,我们可以使用简单的几何图形来处理彼此靠近的对象。例如,我们知道,如果两个边界球的中心之间的距离小于它们半径之和,则这两个球相交。有了包围盒,我们就可以保证在不知道的情况下不会发生碰撞。如果包围体精确地表示对象,碰撞总是准确的。然而,如果包围体大于被保持的物体,当我们认为发生了碰撞(但实际上没有)时,就会有一些误报。被封装的对象离它的包围体越近,我们遇到的相交的假阳性就越少。
限制这种误差的一种方法是将不规则形状的网格分成更小的边界框或球体。随着较小的包围体(或 2D 情况下的区域)的数量增加,误差量减少并且将接近零。图 5-5 显示了 2D 不规则形状和边界矩形,以及几个更接近形状的边界矩形,但也增加了我们必须执行的计算检查的数量。包围矩形内的空白区域显示了误报的碰撞区域。
图 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 左侧所示。
图 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
注意对于更复杂的计算,我们可以使用现有的物理库,比如在第八章的中讨论的那些。
给定初始速度和半径,我们可以计算每个球体的最终速度值,如清单 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 所示。
图 5-7 。典型的投射路径
除非有风或其他水平力,否则水平速度分量 v x 在物体的整个飞行过程中保持不变。由于重力的作用,垂直速度随时间而减小,计算为(v y + a y t)。
注给定一个初始速度矢量 ,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 米
注求抛体的最大高度,可以取初速度 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 右侧显示了显示完整抛射体路径的输出。
图 5-8 。左图:飞行中的弹丸;右图:未清除绘图上下文的射弹
这个演示的完整代码在 05/05 _ propeline . html 文件中。我鼓励你继续玩抛射体和动量游戏。例如,利用这里获得的知识,您可以编写一个简化版的网球程序。
本章的最后一个例子研究了势能和动能之间的关系。
势能
到目前为止,我们一直在看有动能的例子,动能就是运动的能量。另一方面,势能是储存的能量,通常是因为物体的高度和物体自由下落时重力施加的力。势能的一个经典例子是过山车。在过山车的顶部,当汽车静止时,系统中的能量是纯势能(PE)。当每辆车开始下降时,PE 被转换成动能(KE) ,飞车获得速度。当汽车到达地面时,KE 的比值增加,当汽车向上返回时,比值减小。
理论上系统总能量保持不变,如图图 5-9 所示。然而,在现实世界中,能量会因为摩擦力而损失。
图 5-9 。没有摩擦,系统的 PE 和 KE 是守恒的
在我们的下一个例子中,我们将创建一个带有跳跃的小斜坡,并让一个球体沿着它移动。我们可以调整高度来确定一个值,这个值将产生足够的速度使它通过。图 5-10 显示了 2D 计划的坡道轮廓。实际的坡道将是三维的 ,但是像这样的图表对于规划网格是有用的。
图 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 中显示。
图 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§);
在每个动画帧上,检查 与每面墙的碰撞,跟踪我们球体的总速度,并计算 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;
}
实现这种检查的任务留给了读者。如果你雄心勃勃,想要建立一个过山车模型,那么你的计算中需要考虑更多的因素,比如向心力。
摘要
本章介绍了物体的一些物理属性,并模拟了重力、碰撞和抛射。在下一章,我们将讨论分形、高度图和粒子系统的数学主题。在第八章中,当我们介绍一些可以执行复杂得多的计算的物理库时,我们将回到物理。