使用WebGL + Three.js制作动画场景
3D图像,技术,打造产品,还有互联网:这些只是我爱好的一小部分。
现在,感谢WebGL的出现-一个新的javascriptAPI,它可以在不依赖任何插件的情况下渲染浏览器中的3D图像-这让3D渲染操作变得异常简单。
随着虚拟现实和增强现实应用的发展,大型厂商们开始转向数字化触觉体验,这是令人动心的一项技术。
或者,至少那些已经投资的人这一年还抱有希望-11亿美金流入VR和AR领域.
从Abbey Road Studios的谷歌交互之旅到拍摄Deadliest Catch用到的舰队,通过在非真实世界给观众沉浸式的体验,所有的产品,服务和环境得以实现更好的配合。
由于人们能接触到更多体验性的技术,2D开始变得有些单调。这是事实。
让我们现实点。目前看来,很多致力于创造体验的应用仍处在技术探索阶段,对大多数商业领域而言前景不算明朗。
或者说他们真的创造了令人激动的体验吗?
走进WebGL:一项实用与灵活的技术,可以创造更强沉浸式3D内容。无论是Porsche展示一辆新911的细节,还是NASA重点介绍的火星是什么样子,或者是J Dlla备受喜爱的甜甜圈专辑庆典,WebGL能应用于很多领域来表现各种类型的故事。
为让你熟悉这项强大的技术,我打算做一个关于它是如何工作的简要概括,还有使用Three.js(一个基于WebGL API的JavaScript库)一步步创造简单3D环境的快速教程。
首先,什么是WebGL?
WebGL是一项在浏览器中展示基于硬件加速的3D图像的web技术,不需要安装额外插件或者下载多余的软件。
因此,很多受众可以更方便地接触到WebGL。浏览器支持程度也很不错(目前应用广泛),Chrome,Firefox,IE,Opera和Safari等主流移动端和桌面浏览器都提供了很好的支持。
许多计算机和智能手机有先进的图像渲染单元(GPUS),可直到最近,大多数网页和移动网站都不能使用GPUS。这导致设备的加载速度缓慢,图像质量低,并且对3D内容的支持程度也很低。
为了解决这个问题WebGL花了不少时间。基于著名的OpenGL 3D 图像标准,WebGL赋予Javascript插件式的自由接入方式,通过html5 元素连接一个设备的图像硬件,并在浏览器中直接应用3D技术。结果是360度的3D内容变得更容易创建—排除了使用独立应用或插件的干扰——同时用户能更容易地在网上拥有高清体验。
什么是Three.js?
OpenGL和WebGL的复杂度相差不大。
Three.js是一个开源语法库,简化了WebGL工具和环境的创建工作。它支持大部分基于GPU加速的低代码量3D动画。
聊得差不多了,让我们编写代码吧
示例用Three.js库展示了更复杂的效果。为了练习需要,我会尽量写的简单,用低复杂度的环境来展示仅靠理解的基础知识能实现什么效果。
我打算构建一个我们已使用过的例子
让我们开始用了解的基础知识做点东西吧。
一个渲染器,一个场景,还有一个相机
代码链接第一步
贡献者 Matt Agar(@agar)
代码发布于CodePen.
点击并拖动这个例子,做点尝试
CodePen上的例子相当于入门,现在我们开始使用Three.js。
Firstly we need a Scene — a group or stage containing all the objects we want to render. Scenes allow you to set up what and where is going to be rendered by Three.js. This is where you place objects, lights, and cameras.
首先我们需要一个场景 — 一个包含了我们要渲染的所有对象的群组。场景允许你设置Three.js要渲染的对象和渲染位置,以及如何进行渲染。这个场景指的就是你放置对象,光线和相机的地方。
`var scene = new THREE.Scene();`
-用一个好方法创建场景
接下来我们在这个例子中添加一个相机。我添加的是透视相机,但也有其他可用的选项。头两个参数分别指明了相机的视野区域和宽高比。后两个参数代表相机渲染对象的截止距离。
var camera = new THREE.PerspectiveCamera(
75, // Field of view
window.innerWidth/window.innerHeight, // Aspect ratio
0.1, // Near clipping pane
1000 // Far clipping pane
);
// Reposition the camera
camera.position.set(5,5,0);
// Point the camera at a given coordinate
camera.lookAt(new THREE.Vector3(0,0,0));
-添加相机,视场,宽高比和截止距离
最后至关重要的部分是渲染器本身,它掌握着一个来自给定相机视角场景的渲染。Three.js提供了很多种渲染器以供选择,但我决定在这个练习中使用标准的WebGL渲染器。
var renderer = new THREE.WebGLRenderer({ antialias: true });
// Size should be the same as the window
renderer.setSize( window.innerWidth, window.innerHeight );
// Set a near white clear color (default is black)
renderer.setClearColor( 0xeeeeee );
// Append to the document
document.body.appendChild( renderer.domElement );
// Render the scene/camera combination
renderer.render(scene, camera);
-添加渲染器
这个例子也包括了一些基础的几何结构— 在这里是一个扁平的平面 — 我们可以看到一些特征以深度形式被渲染出来。如果没有它,我们只能看到空空的屏幕。我接下来会简短介绍关于Geometry(几何结构),Materials(材质)和Meshes(网格)。
// A mesh is created from the geometry and material, then added to the scene
var plane = new THREE.Mesh(
new THREE.PlaneGeometry( 5, 5, 5, 5 ),
new THREE.MeshBasicMaterial( { color: 0x222222, wireframe: true } )
);
plane.rotateX(Math.PI/2);
scene.add( plane );
-添加一个扁平的平面
一个关于控制相机的小贴士
你可能已经意识到我在这个例子里使用了外部模块。这个是Three.js 的Github repo里能找到的众多可用模块的一个。
在这个例子里是轨道控制,
它允许我们捕获canvas元素上的鼠标事件以重新定位围绕着场景的相机。
var controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.addEventListener( ‘change‘, function() { renderer.render(scene, camera); } );
-实现轨道控制
在CodePen例子中从动作,点击和拖放或者滚动鼠标轮等方面检验轨道控制。在这个示例中,由于我们没有设置动作循环(一旦我开始装饰我的圣诞树,我就会介绍动作循环),当控制发生更新时我们同样需要重新渲染场景。
准备渲染
好吧,之前的例子现在可能看着有点蠢,但你在没有硬件基础的情况下无法构建一个更好的屋子或圣诞树。
是时候给我们的场景添些东西了,现在有三件事需要我们去探索:Geometries,Materials,还有Meshes。
代码链接第二步
贡献者Matt Agar(@agar)
来自Codepen。
-铃儿叮当响。是,它们一定会响的
不论是点击还是拖拽,快尝试一下吧。
使用平面阴影来添加一些简单的多边形
首先我们需要一些Geometry。它可以是包含点和线的任何立方形状。
Three.js简化了一系列可实现的构建基础多边形操作。这里有很多种适合3D格式的文件加载器。你也可以选择通过指定顶点和表面创建你自己的几何结构。
现在,我们将以一个基础的八面体作为开始。
`var geometry = new THREE.OctahedronGeometry(10,1);`
-添加Geometry
Materials描绘了对象的外观。它们的定义不受渲染器影响(大部分情况下),所以当你决定使用不同的渲染器时不必重写它。
这里是可实现的各种Materials,所有的Materials都使用一个包含各种属性的对象,属性会被应用于这些Materials。
下面的例子实现了一个扁平带阴影的Material,这展示了我们的多边形对象,而不是打算对它们进行平滑处理。
var material = new THREE.MeshStandardMaterial( {
color: 0xff0051,
shading: THREE.FlatShading, // default is THREE.SmoothShading
metalness: 0,
roughness: 1
} );
-用Materials确定对象的纹理
第三个我们需要的是Mesh(网格)。一个Mesh就是一个对象,它得到一个多面体并给它应用Material,我们可以把网格插入我们的场景中并自由移动它。
下面是如何来合并Geometry和Material并放入一个Mesh,然后添加到场景中。需要指明的是,将Mesh添加进场景后,我们可以自由地重新定位或者旋转它。
var shapeOne = new THREE.Mesh(geometry, material);
shapeOne.position.y += 10;
scene.add(shapeOne);
-将Geometry和Material合并进一个Mesh中,并将Mesh加入场景
添加光线
一旦我们在场景中拥有了对象,我们需要照亮它们。为了实现这种效果,我们会添加两类不同的光线:环境光和点状光。
环境光的色彩会全局应用到场景中所有的对象。
var ambientLight = new THREE.AmbientLight( 0xffffff, 0.2 );
scene.add( ambientLight );
-给场景添加环境光
点状光在场景中某特定位置创建光。光在任何方向都会闪烁,大概和灯泡的效果类似。
var pointLight = new THREE.PointLight( 0xffffff, 1 );
pointLight.position.set( 25, 50, 25 );
scene.add( pointLight );
-为场景添加点状光
如果这些不能满足你的需求,还有其他种类的光可以选择,包括定向光和斑点光。查看Three.js 光线手册来获得更多信息。
制造并接收阴影
阴影默认是不能使用的,但对创建视觉上的深度很有帮助 — 所以我们需要在渲染器上启用它们。
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
-在渲染器中启用阴影
下一步是指定哪些光线可以形成阴影,还有要渲染的阴影范围有多大。
pointLight.castShadow = true;
pointLight.shadow.mapSize.width = 1024;
pointLight.shadow.mapSize.height = 1024;
-启用光线。相应地,阴影也会出现
最终我们指定哪些网格应该接收阴影。需要指明的是任何网格在不依赖场景的情况下都能制造和接收阴影。
shapeOne.castShadow = true;
shapeOne.receiveShadow = true;
-利用阴影来突出Mesh
在这个场景里,我们使用了一个特殊的阴影Material。它允许一个Mesh仅展示阴影,而非对象本身。
var shadowMaterial = new THREE.ShadowMaterial( { color: 0xeeeeee } );
shadowMaterial.opacity = 0.5;
-实现阴影效果
利用简单要素构建复杂物体
目前为止我们做的一些简单的例子还不错,可如果我们能实现元素复用的话事情会更简单。
代码链接 第三步
代码贡献者 (@agar)
来自 CodePen.
-能确定的是,这些拼合的多边形变得更小巧了
在codepen中点击并拖拽以获得更清晰的效果。
通过对多边形对象进行合并和分层操作,我们可以开始创建更多的复杂形状。
下面的操作是扩展Three.Group对象以求在构造器中创建复杂形状。
var Decoration = function() {
// Run the Group constructor with the given arguments
THREE.Group.apply(this, arguments);
// A random color assignment
var colors = [‘#ff0051‘, ‘#f56762‘,‘#a53c6c‘,‘#f19fa0‘,‘#72bdbf‘,‘#47689b‘];
// The main bauble is an Octahedron
var bauble = new THREE.Mesh(
addNoise(new THREE.OctahedronGeometry(12,1), 2),
new THREE.MeshStandardMaterial( {
color: colors[Math.floor(Math.random()*colors.length)],
shading: THREE.FlatShading ,
metalness: 0,
roughness: 1
} )
);
bauble.castShadow = true;
bauble.receiveShadow = true;
bauble.rotateZ(Math.random()*Math.PI*2);
bauble.rotateY(Math.random()*Math.PI*2);
this.add(bauble);
// A cylinder to represent the top attachment
var shapeOne = new THREE.Mesh(
addNoise(new THREE.CylinderGeometry(4, 6, 10, 6, 1), 0.5),
new THREE.MeshStandardMaterial( {
color: 0xf8db08,
shading: THREE.FlatShading ,
metalness: 0,
roughness: 1
} )
);
shapeOne.position.y += 8;
shapeOne.castShadow = true;
shapeOne.receiveShadow = true;
this.add(shapeOne);
};
Decoration.prototype = Object.create(THREE.Group.prototype);
Decoration.prototype.constructor = Decoration;
-在构造器中创建复杂形状
我们现在能多次复用拼合得到的多边形来给我们的场景添加多重距离,用比单独创建每一个元素更少的工作量让树木更真实。
var decoration = new Decoration();
decoration.position.y += 10;
scene.add(decoration);
-装饰树干
另一个建议是给创建的对象添加一个随机的元素。
在对象的Geometry内移动顶点,以添加一个随机组织的元素来降低形状复杂度。若没有这些小缺陷,做出来的物体会有点合成的感觉。我使用了一个辅助函数来给Geometry的顶点随机添加噪点。
function addNoise(geometry, noiseX, noiseY, noiseZ) {
var noiseX = noiseX || 2;
var noiseY = noiseY || noiseX;
var noiseZ = noiseZ || noiseY;
for(var i = 0; i < geometry.vertices.length; i++){
var v = geometry.vertices[i];
v.x += -noiseX / 2 + Math.random() * noiseX;
v.y += -noiseY / 2 + Math.random() * noiseY;
v.z += -noiseZ / 2 + Math.random() * noiseZ;
}
return geometry;
}
-添加噪点可以使对象更真实
实现动作
目前为止我们只为WebGLRender实现了一个单独的渲染调用。为了向我们的场景中添加一些动作,我们需要做出一些更新。
代码链接 第四步
代码贡献者 (@agar)
来自 CodePen.
-观察下多面体催眠式的缓慢旋转
渲染循环
为了使浏览器适应我们的更新速度,我们正在使用浏览器动作请求框架API来调用一个新的渲染函数。
requestAnimationFrame(render);
function render() {
// Update camera position based on the controls
controls.update();
// Re-render the scene
renderer.render(scene, camera);
// Loop
requestAnimationFrame(render);
}
-利用动作请求框架创建一个渲染循环
超时更新元素
现在,我会对复杂对象做出一些改变,每次创建距离时给装饰物初始化一个随机旋转速度。
this.rotationSpeed = Math.random() * 0.02 + 0.005;
this.rotationPosition = Math.random();
-进入旋转
我们同样设置了一个可以被调用来基于当前值值绕Y轴旋转的新函数。需要指出的是旋转速度基于浏览器取得的帧率,但对这个简单的例子来说还好。对处理这个过程而言,你一定会用到数学函数。
Decoration.prototype.updatePosition = function() {
this.rotationPosition += this.rotationSpeed;
this.rotation.y = (Math.sin(this.rotationPosition));
};
-观察装饰物旋转情况
随着一个更新函数的定义,每运行一次我们就能通过更新渲染循环来重新计算每个元素每次被创建的位置。
function render() {
// Update camera position based on the controls
controls.update();
// Loop through items in the scene and update their position
for(var d = 0; d < decorations.length; d++) {
decorations[d].updatePosition();
}
// Re-render the scene
renderer.render(scene, camera);
// Loop
requestAnimationFrame(render);
}
-重新计算元素位置
把以上的几个例子结合在一起
代码链接 第五步
代码贡献者 (@agar)
来自 CodePen.
-3D圣诞树:完全成型,装饰完美
最终的产品总算出来了。仅仅使用了基础功能,我们已经构建出一个交互式的3D圣诞树,并且建立了一个平面的二维场景。
但这只是使用WebGL的开始。当这项技术快速发展的时候,会出现许多可供选择的资源,还有能正确指导你的教程。以下是资源链接:
The Github repo for three.js, full of examples and endless learning opportunities.
Assorted built-in helpers for cameras, lights, axes etc.
DatGui: create an interface that you can use to modify variables.
stats.js: a handy JavaScript performance monitor (for framerate).
An excellent and very detailed tutorial on creating a Three.js mini game, The Aviator, from the very talented guys at Codrops.
Plus heaps of great low poly examples on Codepen from Karim Maaloul.
你还在等什么?尝试下WebGL和Three.js吧,开始创建你自己的3D效果。如果你做了一些有趣的玩意,请告诉我。我很乐意欣赏一下。
分享
关于作者
拥有超过15年的工程经验,Matt是August的创始成员之一,并团结了世界上一批很优秀的前后端开发者。作为能适应任何情景的问题解决者,Matt从实用角度和大方向上审视项目的技术问题。当他不在解决问题时,他一定在搭建一个虚构的动物王国并和年轻的家人一起探索户外。