《WebGL入门指南》——第2章,第2.4节一个真实的3D示例

简介:

本节书摘来自异步社区《WebGL入门指南》一书中的第2章,第2.4节一个真实的3D示例,作者 【美】Tony Parisi,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.4 一个真实的3D示例
WebGL入门指南
到目前为止,你也许在想“真是个还不错的正方形”,然后开始怀疑我们什么时候开始画一些真正的3D图形。好吧,那就现在吧!在示例2-2中我们将会用更有趣的物体来代替正方形,我们将会完成一个看起来还不错、并且展示了大部分WebGL主要特性、同时还保持代码简洁的页面。

图2-2就是页面的最终效果。其中我们设置了标题文字,添加了一个表面贴有图片的立方体,然后在页面底部也添加了文字。另外值得一提的是,这个页面是可以交互的:点击画布元素,立方体就会开始或停止旋转。


a23d04692db925a1e96232e4547dfd9c628b3796

图2-2 一个进阶的Three.js示例,纹理图像来自http://www.openclipart.org(CC Public Domain Dedication授权使用)

让我们详细看一下这一切是如何完成的。下面是示例2-2的完整代码。这比我们的第一个Three.js示例要复杂一些,但是依然足够简洁,我们可以快速地浏览一下整个代码。

示例2-2 欢迎来到WebGL!

<!DOCTYPE html>
<html>
<head>
<title>Welcome to WebGL</title>
     <link rel="stylesheet" href="../css/webglbook.css" /> 
     <script src="../libs/Three.js"></script>
     <script src="../libs/RequestAnimationFrame.js"></script>
     <script>

     var renderer = null, 
          scene = null, 
          camera = null,
          cube = null,
          animating = false;

     function onLoad()
     {
        // 抓取作为Canvas容器的<div>标签
        var container = document.getElementById("container");
        // 创建Three.js渲染器,并添加到<div>标签中
        renderer = new THREE.WebGLRenderer( { antialias: true } );
        renderer.setSize(container.offsetWidth, container.offsetHeight);
        container.appendChild( renderer.domElement );
        // 创建Three.js场景
        scene = new THREE.Scene();
        // 创建相机,并添加到场景中
        camera = new THREE.PerspectiveCamera( 45, 
            container.offsetWidth / container.offsetHeight, 1, 4000 );
        camera.position.set( 0, 0, 3 );
        // 创建一个平行光光源照射到物体上
        var light = new THREE.DirectionalLight( 0xffffff, 1.5);
        light.position.set(0, 0, 1);
        scene.add( light );
        // 创建一个接受光照并带有纹理映射的立方体,并添加到场景中
        // 首先,创建一个带纹理映射的立方体
        var mapUrl = "../images/molumen_small_funny_angry_monster.jpg";
        var map = THREE.ImageUtils.loadTexture(mapUrl);

        // 然后创建一个Phong材质来处理着色,并传递给纹理映射
        var material = new THREE.MeshPhongMaterial({ map: map });
        // 创建一个立方体的几何体
        var geometry = new THREE.CubeGeometry(1, 1, 1);
        // 将几何体和材质放到一个网格中
        cube = new THREE.Mesh(geometry, material);
        // 设置网格在场景中的朝向,否则我们将不会看到立方体的形状!
        cube.rotation.x = Math.PI / 5;
        cube.rotation.y = Math.PI / 5;
        // 将立方体网格添加到场景中
        scene.add( cube );
        // 添加处理鼠标事件的函数,用于控制动画的开关
        addMouseHandler();
        // 运行渲染循环
        run();
     }
     function run()
     {
          // 渲染场景
          renderer.render( scene, camera );
          // 在下一帧中旋转立方体
          if (animating)
          {
               cube.rotation.y -= 0.01;
          }
          // 在另一帧中回调
          requestAnimationFrame(run); 
     }
     function addMouseHandler()
     {
          var dom = renderer.domElement;

          dom.addEventListener( 'mouseup', onMouseUp, false);
     }

     function onMouseUp  (event)
     {
         event.preventDefault();
         animating = !animating;
     }

     </script>
</head>
<body onLoad="onLoad();" style="">
   <center><h1>Welcome to WebGL!</h1></center>
   <div id="container" 
       style="width:95%; height:80%; position:absolute;">
   </div>
   <div id="prompt" 
      style="width:95%; height:6%; bottom:0; position:absolute;">
   Click to animate the cube
   </div>
</body>
</html>

除去我稍后会详细介绍的一些设置内容和添加了一个CSS样式表来控制字体和颜色之外,这段程序的开头部分和我们之前的那个示例非常相似。我们创建了Three.js渲染器对象,并将其DOM元素作为Canvas容器的子元素加入。这一次我们给构造函数传递了一个名为antialias的参数,并将其设置为true,告诉Three.js要启用抗锯齿(antialiased)渲染。抗锯齿可以避免绘制物体边缘时产生的锯齿。(在Three.js中有很多给方法传递参数的方式。通常来说,在对象中传递构造函数参数都是使用被命名的字段,正如在本示例中这样。)然后,我们创建了一个带透视效果的相机,和之前的示例一样。但这次,相机稍微移动了一些,以便我们可以更近地观察立方体。

2.4.1 为场景着色
我们马上就可以在场景中添加立方体了。如果往前看几行,你会发现我们使用了Three.js内置的CubeGeometry对象来创建一个单位体积的立方体。但在添加立方体之前,我们还需要做其他的一些事情。首先,我们需要对场景进行着色(shading)。如果没有着色,你将无法分辨出立方体表面的边界。我们还需要创建一个纹理映射来渲染到立方体表面,稍后会详细介绍。

为了将着色效果添加到场景中,我们需要做两件事:添加一个光源和使用另一种不同的材质。在Three.js中有好几种不同的光源。在我们的示例中,将会使用平行光(directional light),这是一种从无限远的距离(光源没有特定的位置)照向指定方向的光。Three.js的语法有些奇怪,我们要做的不是设置光源的照射方向,而是要使用position属性设置光源距离原点的位置;由此可以推论出,光源的照射方向就成了从该点指向场景原点(即照射到我们的立方体上)。

我们要的第二件事就是改变使用的材质。在Three.js中,MeshBasicMaterial用于定义属性简单的材质,比如固定颜色或是透明。这种材质不会对光照做出任何响应。所以我们要用另一种类型的材质来代替它,这就是MeshPhongMaterial。这种类型的材质应用了一种相对简单、仿真度高而又性能优越的着色模型,也就是著名的“Phong着色法”(Phong shading)。(Three.js同时还支持其他更加复杂的着色模型,我们之后会讲到。)使用了Phong着色法,我们现在就可以分辨出立方体的边缘。立方体朝向光源方向的面将会更加明亮;而背对光源的面则会相对阴暗。由此,我们即可分辨出面与面之间的边界。

图片 3 也许你已经发现了,在这个章节中我所提到的都是着色,而不是我们在第1章中提到的着色器。这是因为,我们之前说过着色器其实是一小段用类C语言写成的代码,而Three.js已经为我们内置了这样的着色器。我们只要简单的设置光源和材质,Three.js就可以用内置的着色器来替我们处理剩下的脏活累活。感谢Three.js的作者,感谢Mr.doob!

2.4.2 添加纹理映射
纹理映射(texture map),也被称为纹理(texture),是一种将位图覆盖到3D网格表面的技术。使用纹理映射可以通过简单的方式定义网格表面的颜色,同时也可以通过纹理图片的组合应用创造出更加复杂的效果,如凹凸效果和高光效果。WebGL提供了一些API调用来处理纹理,同时因为安全原因,限制了诸如跨域纹理的使用(详细信息参见第7章)。幸运的是,在Three.js中我们只要使用简单的API就可以载入纹理并与材质绑定在一起,而不需要处理太多的琐事。我们调用THREE.ImageUtils.loadTexture()方法来从一个图片文件载入纹理,然后通过向材质的构造函数传递map参数,把处理后的纹理与材质相关联。

在这里,Three.js替我们完成了很多的底层工作。它会把JPEG图像正确的映射到立方体的各个面上,并保证图像不会拉伸覆盖所有面,同时在各个面上的图像不会出现翻转或者倒立的错误。也许看起来这并不是什么大事情,但是如果你亲自用WebGL原生API来完成这件事的话,你会发现需要处理的细节太多了。而Three.js再一次替我们完成了这些困难的工作,它用内置的Phong着色器结合光源的设置、材质颜色和纹理映射的像素值,使得最终每个像素上都显示正确的颜色,并形成最后的图像效果。在Three.js中,我们可以使用纹理映射做很多事情。我们将会在随后的章节中详细讨论更多细节。

现在可以开始创建我们的立方体网格了。我们构造了一个几何体、材质和纹理,然后把它们都放到一个Three.js的网格中,并存储在变量cube中。示例2-3列出了用于创建带光照效果、纹理和Phong着色的立方体的代码。

示例2-3 创建带光照效果、纹理和Phong着色的立方体

// 创建照射物体的平行光光源
var light = new THREE.DirectionalLight( 0xffffff, 1.5);
light.position.set(0, 0, 1);
scene.add( light );
// 创建一个带着色效果和纹理映射的立方体,并添加到场景中
// 首先,创建纹理映射
var mapUrl = "../images/molumen_small_funny_angry_monster.jpg";
var map = THREE.ImageUtils.loadTexture(mapUrl);
// 然后创建一个Phong着色材质,并与纹理关联
var material = new THREE.MeshPhongMaterial({ map: map });
// 创建一个立方体的几何体
var geometry = new THREE.CubeGeometry(1, 1, 1);
// 将几何体和材质放入同一个网格中
cube = new THREE.Mesh(geometry, material);

2.4.3 旋转物体
图片 3在我们能看到立方体之前,还需要做一件事,那就是稍微旋转一下它。否则我们永远也不会发现这是一个立方体,因为它会端正地用一个面朝向我们,看起来就和我们在之前的示例中画的那个正方形一模一样,只是多了纹理贴图。所以让我们把它朝相机方向绕着x轴(水平方向)翻转一些。我们是通过设置网格的rotation属性来完成的。在Three.js中,每个物体都有位置(position)、旋转(rotation)和缩放(scale)属性。(还记得吗?我们在之前的示例中,把相机往后移动了一些。)通过给rotation.x赋一个非零值,我们告诉Three.js去为物体绕着x轴做当量的旋转。同理我们也以此处理y轴,让立方体稍微向左旋转一下。这样一来,我们就可以看到立方体6个面中的3个了。

在为物体的旋转变量赋值时,我们需要注意,大部分的3D图形系统都使用了弧度制(radians)来度量角度。弧度是指单位圆上相应角度的圆弧长度(例如,弧度制的2π就是角度制的360°)。Math.PI相当于180°,因此当我们赋值mesh.rotation.x = Math.PI / 12的时候,实际上是绕着x轴旋转了15°。

2.4.4 循环重绘和requestAnimationFrame()
你也许已经发现这个示例与之前的那个在结构上有些不同。首先,我们增加了一些辅助性的函数。其次,我们定义了一些全局变量来存储那些将用于辅助性函数的值。(好吧,我知道,这样滥用全局变量又是一个不好的习惯。但就像我承诺过的一样,在下一章我们将会应用框架结构。)同时我们还增加了一个循环函数,这就是所谓的循环重绘。通过循环重绘,我们不再只渲染一次场景,而是持续不断地渲染。这对于静态场景并不重要,但是对于含有运动物体或响应用户输入而发生变化的场景来说,我们需要持续不断地渲染场景。从现在开始,我们以后所有的示例都将使用循环重绘来渲染场景。

有很多方法来应用循环重绘。一种方法就是使用setTimeout()回调,每当场景渲染完毕后就重置超时时差。这也是一种经典的Web开发技巧,用于制作动态效果;然而它的确已经过时了,因为新的浏览器都支持一种更好的方法,那就是requestAnimationFrame()。这个函数被专门设计用于制作页面动画,当然也包括WebGL中的动画。

使用requestAnimationFrame(),浏览器可以极大地优化动画的性能表现。因为它会综合考虑所有的绘制请求,把它们都放到同一个重绘步骤中。尤其在多标签浏览器中,当动画页面处于后台时,浏览器将停止重绘以节省资源提高性能。不过并不是所有版本的所有浏览器都支持这个函数;更加添乱的是,每个浏览器中这个函数的函数名都不一样。因此,我引入了一个漂亮的工具:由Paul Irish编写的RequestAnimation Frame.js。这个文件会掩盖不同浏览器的差异性,开发者只要简单地使用requestAnimationFrame()即可。

我们已经准备好要开始渲染场景了。我们定义了一个函数run(),负责进行循环重绘。和之前一样,我们调用了渲染器的render()方法,将场景和相机传递给它。然后,我们写了一小段逻辑代码让立方体可以动起来。我们将会在下一节详细介绍。最后我们让浏览器继续渲染下一帧。参见示例2-4。

示例2-4 循环重绘

function run()
{
// 渲染场景
renderer.render( scene, camera );
// 为下一帧旋转立方体
if (animating)
{
    cube.rotation.y -= 0.01;
}
// 请求下一帧
requestAnimationFrame(run);
}

你可以在图2-2中看到最后的运行效果。现在你可以看到立方体的前侧和顶部。我们终于在页面上显示了一个真正的3D物体!

2.4.5 让页面贴近生活
作为第一个完整的示例,我们原本可以到此就结束了。我们已经在页面上绘制了一个漂亮的图形,而且是真正的3D图形。但是在今天,3D图形不只是和渲染有关了,它还需要动画和交互性。如果没有动画和交互,开发者只需要让做3D美术建模的朋友在Max和Maya中渲染好之后,存为图片然后用 标签嵌入网页就好,何必还折腾什么WebGL呢?这不是我们想要的。因此在这种思想的指导下,即使是一个简单的示例,我们也要为它加入动画和交互。

在之前的小节中,我们讨论了循环重绘。这就是我们在渲染下一帧之前改变场景的机会。为了旋转这个立方体,我们需要在每一帧都改变它的旋转属性值。我们不想让它随机乱转,而是一个绕着y轴平滑的旋转,所以我们需要做的就是每过一段时间就给它的旋转的y值做适度的增量。在WebGL中,这是一种相对简单的制作动画效果的思路。当然,还有其他的方法,但都更加复杂。我们会在以后的章节中介绍。

最后,如果我们可以控制立方体的旋转那就再好不过了。我们已经添加了一个处理鼠标点击的函数,直接使用了DOM的事件方法。其中有一个需要指出的小技巧就是,在这个示例中我们使用的DOM元素是和Three.js中渲染器对象关联的。具体的代码参考示例2-5。

示例2-5 添加鼠标交互

function addMouseHandler()
{
    var dom = renderer.domElement;
    dom.addEventListener( 'mouseup', onMouseUp, false);
}
function onMouseUp    (event)
{
    event.preventDefault();
    animating = !animating;
}

点击立方体吧!看着它旋转!是不是很沉浸其中呢?

相关文章
|
机器学习/深度学习 人工智能 自然语言处理
浅析人机对话系统的主要模块及核心技术
之前,在我的另一篇博客:简述智能对话系统 里面概述了对话系统的分类、应用场景及产生的社会价值。今天,来简单讲述一下对话系统的主要模块与核心技术。
各个国家缩写域名后缀列表(全球)
不同的国家分属不同的国家后缀域名,例如中国的国家后缀域名为- .cn,云吞铺子分享全球各个国家的国家域名后缀表: 国家域名后缀列表 以下国家的域名,按照域名缩写的字母排序: A .ac 亚森松岛 .
33246 0
|
机器学习/深度学习 人工智能 Linux
Fish Speech 1.5:Fish Audio 推出的零样本语音合成模型,支持13种语言
Fish Speech 1.5 是由 Fish Audio 推出的先进文本到语音(TTS)模型,支持13种语言,具备零样本和少样本语音合成能力,语音克隆延迟时间不到150毫秒。该模型基于深度学习技术如Transformer、VITS、VQVAE和GPT,具有高度准确性和快速合成能力,适用于多种应用场景。
1514 3
Fish Speech 1.5:Fish Audio 推出的零样本语音合成模型,支持13种语言
|
机器学习/深度学习 人工智能 搜索推荐
《基因测序新视界:人工智能的关键赋能》
基因测序是解密生命密码的关键技术,开启了疾病诊断与个性化医疗的新纪元。然而,随着数据量的爆炸式增长,传统分析方法难以应对。人工智能(AI)凭借强大的模式识别和数据处理能力,在基因测序数据分析中崭露头角。AI不仅提高了疾病诊断的准确性和效率,还在药物研发、基因调控网络构建等领域发挥了重要作用。通过AI,研究人员能快速筛选药物靶点、预测药物反应,并揭示基因间的复杂调控机制。此外,AI在群体遗传学和进化生物学中的应用也取得了显著进展。尽管面临数据隐私和模型可解释性等挑战,AI已成为推动基因测序分析发展的关键力量,为人类健康和生命科学带来革命性变化。
503 18
|
JSON 自然语言处理 Java
OpenAI API深度解析:参数、Token、计费与多种调用方式
随着人工智能技术的飞速发展,OpenAI API已成为许多开发者和企业的得力助手。本文将深入探讨OpenAI API的参数、Token、计费方式,以及如何通过Rest API(以Postman为例)、Java API调用、工具调用等方式实现与OpenAI的交互,并特别关注调用具有视觉功能的GPT-4o使用本地图片的功能。此外,本文还将介绍JSON模式、可重现输出的seed机制、使用代码统计Token数量、开发控制台循环聊天,以及基于最大Token数量的消息列表限制和会话长度管理的控制台循环聊天。
4417 7
|
Docker Windows 容器
手把手教您在 Windows Server 2019 上使用 Docker
现在,您可以直接用 Windows Server 来运行“纯”Docker 容器,其中所有的容器进程都可以直接在主机操作系统上运行。
27348 1
|
存储 缓存 JavaScript
三个小时vue3.x从零到实战(前)(vue3.x基础)
该文章系列提供了Vue3.x从基础到实战的教程,涵盖安装、基本语法、组件化应用及项目构建等多个方面,适合从零开始学习Vue3.x的开发者。
2369 0
|
SQL 网络协议 网络安全
【Python】已解决:pymssql._pymssql.OperationalError: (20009, b’DB-Lib error message 20009, severity 9:\nUn
【Python】已解决:pymssql._pymssql.OperationalError: (20009, b’DB-Lib error message 20009, severity 9:\nUn
761 0
|
机器学习/深度学习 人工智能 搜索推荐
构建未来:基于AI的自适应学习系统
【4月更文挑战第28天】 随着人工智能技术的不断进步,其在教育领域的应用也日益广泛。本文将探讨如何利用AI技术构建一个自适应学习系统,以提供更加个性化的学习体验。我们将讨论AI在教育中的应用,包括智能教学系统的设计、学习内容的个性化推荐以及学习进度的自动调整等方面。此外,我们还将探讨如何通过数据分析来优化学习过程,以及如何保护学习者的隐私。
668 0
|
JavaScript 小程序 API
uniapp中的uview组件库丰富的Form 表单用法
uniapp中的uview组件库丰富的Form 表单用法
1613 0