第七章 使用WebGL和ThreeJS绘制3D图形
概述
WebGL是Web上的3D实现,正如其名称所示,它运用到OpenGL,OpenGL是硬件加速3D图形的标准。相较2d而言,3D在一些方面会复杂很多,复杂不仅体现在我们需要处理3维坐标系统及相关的数学公式,我们还需要关心形状的状态,这可远比2D时颜色及变换复杂。
在处理2D内容时,我们只需要绘制形状并用不同的样式来填充,这非常简单。但是当我们处理3D图形时就涉及到复杂的多维度的控制。
首先,我们需要利用几何学知识绘制其形状,首先3维空间的点称为向量,然后我们需要为形状添加其它信息,比如说表面光的反射方向,然后我们需要设置光源位置和相机位置,相机位置决定了我们的观看位置,光源位置决定了光是从那里照过来的,在做完这些以后,我们还需要设置明暗器。
明暗器接受相机,光照,表面反射情况和几何形状为参数来绘制真实的像素。有两种类型的着色器,一种用来修改向量来实现最终光的反射情况,另一种被称为像素明暗器用以绘制实际的像素。
着色器是由一种类似于C的特殊OpenGL语言编写的小的程序,由于涉及到大规模的并行,这种代码很难编写。现代的图形处理器具有超强并行多核芯片,他们处理一件事情时非常高效,可以快速的绘制很多像素。
着色器是现代图形处理器的能力之源,但是他们用起来并不简单,不过优点在于你可以在自己的应用中运用着色器实现很多非常令人惊讶的效果,但是缺点在于你必须为你的应用安装自己的着色器,在WebGL标准中并未附带任何内置的着色器。
上面描述就是 OpenGL ES 2.0 and OpenGL 3(不带着色器的老版本)的工作原理。这是一个灵活又复杂的系统,WebGL也是类似的,只是这里用的是JavaScript控制而非C语言控制。
在这里我们不会教授你OpenGL的原理,这一点学起来很复杂(学习可能需要花费整整一周的时间),就算学会了OpenGL的基本概念,可能编写一个非常简单的游戏,也需要你写下数千行代码,我们还是建议你使用相关的库及图形引擎来做这些底层的事情,而你只需要专注于你的应用的实际效果。在WebGL领域,最受推荐的库是ThreeJS 。这个库自带可复用的着色器,大大简化了构建可交互的3DApp的难度。
一些例子
我们来看一些用three.js 实现的例子。 上面这个游戏叫做Zombies vs Cow,游戏中你可以使用方向键让牛避免被僵尸咬到,这是一个纯粹的3D游戏由硬件加速。这个看起来像是Wii上的专业游戏,但是实际上这个真的是有web技术编写出来的。
原文中提供的原游戏链接已经失效。。
下面这个例子是一个web版的谷歌地球,你可以体验一下,体验和安装版的谷歌地球app类似。
下面这个例子是一个3D可视化的音频呈现程序。
浏览器的支持情况
原文中的支持情况已经过时,目前的支持情况如下,可以看出,其在新版浏览器中还是有很好的支持的。
一个ThreeJS
使用示例
ThreeJS是由富有创造力的开发者Mr.Doob创建的开源库,点击链接,你可以看到很多他的优秀的作品。顺便说一下,他的真名是Ricardo Cabello。ThreeJS是基于WebGL开发的,这是一个可以让你专注于开发的库,为了便于入门,他还写了一个好的例子 Boilerplate Builder for three.js,这个例子涉及到相机,鼠标输入和渲染。模仿这个例子可以让你快速上手Three.js.这个模板有很多可供调节的选项,但是现在你就采用默认的就可以了。
我们一起来尝试一下,首先到 Boilerplate Builder for three.js下载一个模板,解压运行一下,你应该可以看到如下的效果。
打开index.html
,我们可以看到这个模板具有良好的注释,我们从init()
函数开始分析:
// init the scene
function init(){
if( Detector.webgl ){
renderer = new THREE.WebGLRenderer({
antialias : true, // to get smoother output
preserveDrawingBuffer : true // to allow screenshot
});
renderer.setClearColorHex( 0xBBBBBB, 1 );
// uncomment if webgl is required
//}else{
// Detector.addGetWebGLMessage();
// return true;
}else{
renderer = new THREE.CanvasRenderer();
}
renderer.setSize( window.innerWidth, window.innerHeight );
document.getElementById('container').appendChild(renderer.domElement);
首先,模板初始化了整个环境,它试图创建webGl渲染,因为ThreeJS其实是支持其它一些备选的渲染方案的(比如Canvas),首先我们尝试创建WebGL,如果不能调用WebGL渲染,会转而调用2D Canvas渲染,虽然Canvas会慢很多,但是总比什么也不显示要好,具体如何这个还是得取决于你。
然后会设置Canvas的大小并添加其为container
元素的一个子元素
// add Stats.js - https://github.com/mrdoob/stats.js
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.bottom = '0px';
document.body.appendChild( stats.domElement );
接下来我们创建了一个Stats
对象,并把它添加到了屏幕上,这个对象用以告诉我们代码运行得有多快。
// create a scene
scene = new THREE.Scene();
接下来我们新建一个Sence
,ThreeJS运用一种被称为场景图的树形结构,Sence
是树形结构的根节点,我们在屏幕上创建的所有子图形都会是场景树的子节点。
// put a camera in the scene
camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set(0, 0, 5);
scene.add(camera);
接下来就是相机了,这里创建的是透视相机,一般说来 你可以保持这些默认值,但是如果你愿意,你也可以改变相机的位置。
// create a camera contol
cameraControls = new THREEx.DragPanControls(camera)
DragPanControls
是一种允许你通过拖动鼠标改变相机位置的组件,当然你也可以选用其它的控制组件。
// transparently support window resize
THREEx.WindowResize.bind(renderer, camera);
// allow 'p' to make screenshot
THREEx.Screenshot.bindKey(renderer);
// allow 'f' to go fullscreen where this feature is supported
if( THREEx.FullScreen.available() ){
THREEx.FullScreen.bindKey();
document.getElementById('inlineDoc').innerHTML += "- f for fullscreen";
}
一般说来我们可以手动控制窗口变化,但是Threex.WindowResize
对象会为我们自己动处理这个(这个对象有模板提供,并不是由threeJS提供)。它的作用是调整场景的大小来适配窗口大小,接下来的两行绑定了快捷键f来全屏,绑定了快捷键p来截图。
至此我们已经做完了基本设置,接下来我们在场景中添加图形,我们试着添加一个圆环(甜甜圈形),ThreeJS支持多种标准形状,圆环就是其中之一。
// here you add your objects
// - you will most likely replace this part by your own
var geometry = new THREE.TorusGeometry( 1, 0.42 );
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
场景中的一个对象被称为mesh
,一个mesh
由几何形状和材质两部分组成,模板使用环形和标准材质,这种材质直接反射表面光而不去设置颜色。上述代码描述了如何创建mesh
并添加到场景中。
// animation loop
function animate() {
// loop on request animation loop
// - it has to be at the begining of the function
// - see details at http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
requestAnimationFrame( animate );
// do the render
render();
// update stats
stats.update();
}
接下来我们来看animate
函数,这个函数通过requestAnimationFrame
自调用,触发render()
更新stats
.
// render the scene
function render() {
// update camera controls
cameraControls.update();
// actually render the scene
renderer.render( scene, camera );
}
render
函数每帧都会调用,首先它会更新相机控制,使得相机能随着鼠标和键盘移动,然后这个函数调用renderer.render
绘制场景到屏幕中。
下图是效果图:
自定义模板
接下来我们再添加一些自定义,场景中的每一个对象都是可以进行基本的缩放,旋转和位置变换的。我们可以使用mesh.rotation.y=Math.PI/2
让我们的弧形旋转,注意弧形旋转的角度是弧度而不是角度。
var geometry = new THREE.TorusGeometry( 1, 0.42 );
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh( geometry, material );
mesh.rotation.y = Math.PI/2; //90 degrees
我们绘制一些更复杂的形状来替换现有的弧形,ThreeJS支持使用预制模块,在图形世界里,绘制3D茶壶就类似于编码中的hello world
,我们来试一下。茶壶是一个JSON文件,我们可以从这儿下载源代码,并把它放置在index.html中相同的位置。然后我们调用THREE.JSONLoader().load()
加载它。当加载结束,我们就成功把它也添加到了场景中了。
//scene.add( mesh );
new THREE.JSONLoader().load('teapot.js', function(geometry) {
var material = new THREE.MeshNormalMaterial();
var mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
teapot = mesh;
});
接下来我们为其添加一些动画,使得茶壶每一帧都在旋转。我们为其添加一个变量,使每一帧旋转0.01度即可。
// update camera controls
cameraControls.update();
teapot.rotation.y += 0.01;
着色器效果
最后我们添加一些额外的处理效果(post-processing
),被称作post-processing
的原因是因为其发生在主渲染之后。这部分的ThreeJS API还是实验性的,文档也还不算完善,但是这里我们还是来见识一下其强大的效果。使用后处理,需要在我们的页面中引入额外的脚本,这里我们需要ShaderExtras.js
,RenderPass.js
,BloomPass.js
,ShaderPass.js
,EffectComposer.js
,DotScreenPass.js
以及MaskPass.js
.
<script src="vendor/three.js/ShaderExtras.js"></script>
<script src="vendor/three.js/postprocessing/RenderPass.js"></script>
<script src="vendor/three.js/postprocessing/BloomPass.js"></script>
<script src="vendor/three.js/postprocessing/ShaderPass.js"></script>
<script src="vendor/three.js/postprocessing/EffectComposer.js"></script>
<script src="vendor/three.js/postprocessing/DotScreenPass.js"></script>
<script src="vendor/three.js/postprocessing/MaskPass.js"></script>
我们首先创建一个名为initPostProcessing()
的函数,在其中我们创建已成而效果合成器
function initPostProcessing() {
composer = new THREE.EffectComposer(renderer);
接下来,我们添加一个render pass
使得渲染整个场景为一个具有质地的图片。我们需要告诉它不会被渲染到场景上,然后再添加到合成器中。
renderModel = new THREE.RenderPass(scene,camera);
renderModel.renderToScreen = false;
composer.addPass(renderModel);
接下来我们需要添加dot screen pass
,它的默认效果很棒,但是你还是可以依据你的需求进行调整,这个处理需要显示在场景中,所以我们需要把renderToScreen
设置为true
,然后添加其到合成器中。
var effectDotScreen = new THREE.DotScreenPass(
new THREE.Vector2(0,0), 0.5, 0.8);
effectDotScreen.renderToScreen = true;
composer.addPass(effectDotScreen);
然后我们需要更新render
函数,在这里我们不用renderer.render()
,我们先调用renderer.clear()
然后调用composer.render()
;
// actually render the scene
//renderer.render( scene, camera );
//alt form
renderer.clear();
composer.render();
最后我们需要在init
函数最后调用initPostProcessing
。
initPostProcessing();
效果图如下:
为了满足你的好奇心,我们打开ShaderExtras.js
看看实际的着色器是什么样子,代码如下,这段代码描述了点是如何生成的:
fragmentShader: [
"uniform vec2 center;",
"uniform float angle;",
"uniform float scale;",
"uniform vec2 tSize;",
"uniform sampler2D tDiffuse;",
"varying vec2 vUv;",
"float pattern() {",
"float s = sin( angle ), c = cos( angle );",
"vec2 tex = vUv * tSize - center;",
"vec2 point = vec2( c * tex.x - s * tex.y, s * tex.x + c * tex.y ) * scale;",
"return ( sin( point.x ) * sin( point.y ) ) * 4.0;",
"}",
"void main() {",
"vec4 color = texture2D( tDiffuse, vUv );",
"float average = ( color.r + color.g + color.b ) / 3.0;",
"gl_FragColor = vec4( vec3( average * 10.0 - 5.0 + pattern() ), color.a );",
"}"
].join("\n")
一些细节问题
类似于OpenGL
,WebGL也不能直接支持text
。如果你需要使用文字,你需要使用Canvas。
有一个快速的GUI库叫做dat-gui
,其主页在这里.
存在很多支持不同格式的loader
,你可能会用到Collada
或JSON loaders
(dae格式需要使用Collada),一些只包含几何形状,一些还包含质地和动画(monster)。loader很重要,因为很多复杂的几何图形不能由代码直接创建,相反你需要使用其他人创建的形状,很可能是类似于Blender
或Maya
这样的3D工具。
对于大部分情况而言,关于OpenGL的性能优化技巧也可以应用到WebGL中,比如说,你需要缓存形状和材质在GPU中。
CreativeJS具有很多很好的WebGL和2D Canvas的例子。
下一章中,我们绘制一个在蓝天草坪上运动的3D汽车。