第一个3D场景
源码下载下面的代码完整展示了通过three.js引擎创建的一个WebGL三维场景,在场景中绘制并渲染了一个立方体的效果,为了大家更好的宏观了解three.js引擎, 尽量使用了一段短小但完整的代码实现一个实际的三维效果图。随着时间的推移会在Web上实现各种各样的功能,有很多内容已经超出传统的前端范畴, 术业有专攻,每个人的基础不同,刚一开始学习,不需要完全看懂下面的代码,能够修改增删代码就可以,随着时间的推移就能够很好的使用WebGL三维引擎three.js。
首先需要创建一个.html文档,然后在使用three.js的构造函数、方法和属性之前要先引入three.js或three.min.js文件,three.min.js是three.js压缩后的文件, 可以利用http通信远程加载three.js文件调试代码,也可以放在本地文件夹中调试代码。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <style> 7 body{ 8 margin: 0; 9 overflow: hidden;//隐藏body窗口区域滚动条 10 } 11 </style> 12 <!--引入three.js三维引擎--> 13 <script src="http://www.yanhuangxueyuan.com/3D/example/three.min.js"></script> 14 </head> 15 <body> 16 <script> 17 /** 18 * 创建场景对象 19 */ 20 var scene=new THREE.Scene(); 21 /** 22 * 创建网格模型 23 */ 24 var box=new THREE.BoxGeometry(100,100,100);//创建一个立方体几何对象 25 var material=new THREE.MeshLambertMaterial({color:0x0000ff});//材质对象 26 var mesh=new THREE.Mesh(box,material);//网格模型对象 27 scene.add(mesh);//网格模型添加到场景中 28 /** 29 * 光源设置 30 */ 31 //点光源 32 var point=new THREE.PointLight(0xffffff); 33 point.position.set(400,200,300);//点光源位置 34 scene.add(point);//点光源添加到场景中 35 //环境光 36 var ambient=new THREE.AmbientLight(0x444444); 37 scene.add(ambient); 38 /** 39 * 相机设置 40 */ 41 var width = window.innerWidth;//窗口宽度 42 var height = window.innerHeight;//窗口高度 43 var k = width/height;//窗口宽高比 44 var s = 100;//三维场景缩放系数 45 //创建相机对象 46 var camera=new THREE.OrthographicCamera(-s*k,s*k, s,-s,1,1000); 47 camera.position.set(200,300,200);//设置相机位置 48 camera.lookAt(scene.position);//设置相机方向(指向的场景对象) 49 /** 50 * 创建渲染器对象 51 */ 52 var renderer=new THREE.WebGLRenderer(); 53 renderer.setSize(width,height); 54 renderer.setClearColor(0xb9d3ff,1);//设置背景颜色 55 document.body.appendChild(renderer.domElement);//body元素中插入canvas对象 56 //执行渲染操作 57 renderer.render(scene,camera); 58 </script> 59 </body> 60 </html>
体验测试
直接看上面的代码大家可能不太理解,如果是第一次接触会比较陌生,可以尝试更改代码的参数看看有什么效果,代码的功能都有注释, 看着注释也能大概猜出一个参数的含义。通过更该代码同时刷新浏览器查看效果形成一个互动来提高自己学习的驱动力。
几何体Geometry
第24行代码通过构造函数THREE.BoxGeometry(100,100,100)创建了一个长宽高都是100的立方体,通过构造函数名字中的BoxGeometry也能猜出这个构造函数的意义,利用new关键字操作构造函数可以创建一个对象, 这都是Javascript语言的基本知识,至于THREE.BoxGeometry()构造函数具体是什么可以不用关心, 就像你使用前端使用JQuery库一样查找官方文档就可以,你可以把代码THREE.BoxGeometry(100,100,100)中的第一个参数更改为为50,刷新浏览器查看数据更改后长方体的效果图,可以看到已经不是长宽高一样的立方体, 而是普通的长方体。
材质Material
第25行代码通过构造函数THREE.MeshLambertMaterial({color:0x0000ff})创建了一个可以用于立方体的材质对象, 构造函数的参数是一个对象,对象包含了颜色、透明度等属性,本案例中只定义了颜色color,颜色属性值是0x0000ff表示红色,可以把颜色值改为0x00ff00,可以看到是绿色的立方体效果, 这里使用的颜色值表示方法是16进制RGB三原色模型。使用过渲染类软件、设计过网页或者学习过图形学应该能知道RGB三原色模型,这里就不再详述。
光照Light
第32行代码通过构造函数THREE.PointLight(0xffffff)创建了一个点光源对象,参数0xffffff定义的是光照强度, 你可以尝试把参数更改为为0x444444,刷新浏览器你会看到立方体的表面颜色变暗,这很好理解,实际生活中灯光强度变低了,周围的景物自然暗淡,具体的WebGL光照模型算法three.js都进行了封装,不需要你了解计算机图形学, 可以直接使用,就像你使用普通的三维建模渲染软件一样,只是这里多了一个Javascript编程语言而已。
相机Camera
第46行代码通过构造函数THREE.OrthographicCamera()创建了一个正射投影相机对象, 什么是“正射投影”,什么是“相机对象”,每个人的基础不一样,或许你不太理解,或许你非常理解,如果不清楚还是那句话,刚一开始不用深究,改个参数测试一下看看视觉效果你就会有一定的感性认识。 比如把该构造函数参数中用到的参数s,也就是第44行代码var s = 100;中定义的一个系数,可以把100更改为200,你会发现立方体显示效果变小,这很好理解,相机构造函数的的前四个参数定义的是拍照窗口大小, 就像平时拍照一样,取景范围为大,被拍的人相对背景自然变小了。第47和48行代码定义的是相机的位置和拍照方向,可以更改第47行代码定义的相机位置,把第一个参数也就是x坐标从200更改为250, 你会发现立方的在屏幕上呈现的角度变了,这就像你生活中拍照人是同一个人,但是你拍照的位置角度不同,显示的效果肯定不同。这些具体的参数细节可以不用管, 至少你知道相机可以缩放显示三维场景、对三维场景的不同角度进行取景显示。
three.js程序结构图树状图
场景——相机——渲染器
从实际生活3D场景创建的角度理解上面的程序,就像你使用3Dmax、blender、Rhino、Solidworks等各个领域的三维软件一样去理解,也就是说立方体网格模型和光照组成了一个虚拟的三维场景, 相机对象就像你生活中使用的相机一样可以拍照,只不过一个是拍摄真实的景物,一个是拍摄虚拟的景物,拍摄一个物体的时候相机的位置和角度需要设置,虚拟的相机还需要设置投影方式, 当你创建好一个三维场景,相机也设置好,就差一个动作“咔”,通过渲染器就可以执行拍照动作。
对象、方法和属性
从面向对象编程的角度理解上面的程序,使用three.js和使用其它传统前端Javascript库或框架一样,通过框架提供的构造函数可以创建对象,对象拥有方法和属性,只不过three.js是一款3D引擎, 如果你对HTML、Javascript语言、三维建模渲染软件都能够理解应用,即使你不懂计算机图形学和WebGL,也能够学习three.js引擎,创建可以在线预览的三维场景。
代码第20、46、52行分别使用构造函数THREE.Scene()、THREE.OrthographicCamera()、THREE.WebGLRenderer()创建了场景、相机、渲染器三个最顶层的总对象,然后通过总对象的子对象、方法和属性进行设置, 相机对象和渲染对象相对简单,最复杂的是场景对象,第26行使用构造函数Mesh()创建了一个网格模型对象,该对象把上面两行含有顶点位置信息的几何体对象和含有颜色信息的材质对象作为参数,网格模型创建好之后, 需要使用场景对象的方法.add()把三维场景的子对象添加到场景中,第32、36行定义了两个点光源、环境光对象,然后作为场景的子对象插入场景中。 场景、相机、渲染器设置完成后,定义第57行代码renderer.render(scene,camera)把场景、相机对象作为渲染器对象方法render()的参数,这句代码的意义相当于告诉浏览器根据相机的放置方式拍摄已经创建好的三维场景对象。
WebGL封装
从WebGL的角度来看,three.js提供的构造函数基本是对原生WebGL的封装,如果你有了WebGL的基础,在学习three.js的很多对象、方法和属性是很容易理解的。在three.js入门教程中不会去过多讲解WebGL的基础知识, 但是为了大家更好的理解three.js的很多命令,与three.js相关的WebGL API知识、GPU渲染管线的知识。图形学可能很多人会觉得比较难,其实主要是算法部分,大家先可以学习一些基本的WebGL知识,初学的时候尽量不关注算法,主要了解顶点数据处理的过程,GPU渲染管线的基本功能单元。实际的工作中如果不是开发3D引擎可能不会使用原生WebGL API,但是学习了这些之后,对于three.js的深度开发学习很有好处,如果你了解你WebGL知识,可以联系绘制函数drawArrays()来理解渲染器的渲染操作方法render()。
3D场景中插入新的几何体
上面的代码中绘制了一个立方体,下面通过three.js的球体构造函数SphereGeometry()在三维场景中添加一个球几何体。
构造函数
SphereGeometry(radius, widthSegments, heightSegments)
第一个参数radius约束的是球的大小,参数widthSegments、heightSegments约束的是球面的精度,球体你可以理解为正多面体,就像圆一样是正多边形,当分割的边足够多的时候,正多边形就会无限接近于圆,球体同样的的道理, 有兴趣可以研究利用WebGL实现它的算法,对于three.js就是查找文档看使用说明。
参数 | 含义 |
radius | 球体半径 |
widthSegments | 控制球面精度,水平细分数 |
heightSegments | 控制球面精度,水平细分数 |
绘制球体
首先第一步可以先绘制一个球体替换上面的立方体,然后再尝试同时绘制立方体和球体。使用下面的代码替换原来代码中第24行代码即可。
var box=new THREE.SphereGeometry(60,40,40);//创建一个球体几何对象
同时绘制立方体、球体
这也比较简单,直接模仿立方体的代码就可以,需要创建一个几何体对象作和一个材质对象,然后把两个参数作为构造函数Mesh()的参数创建一个球体网格模型,然后再使用场景对象scene的方法.add()把网格模型加入场景中。
下面代码定义了一个红色球体,即使你不知道具体的细节,但是你只要会模仿立方体复制他的代码修改会球体,本节课的目的就达到了。把下面的代码插入到立方体的代码后面,刷新页面可查看效果。
//球体网格模型 var sphere=new THREE.SphereGeometry(60,40,40);//创建一个球体几何对象 var sphereMaterial=new THREE.MeshLambertMaterial({color:0xff0000});//材质对象 var spereMesh=new THREE.Mesh(sphere,sphereMaterial);//网格模型对象 scene.add(spereMesh);//网格模型添加到场景中
刷新页面你会发现球体和立方体叠加在在一起了,这个很好理解,three.js提供的常用几何体对象默认几何中心与坐标原点重合,这时候使用网格模型对象的方法translateY()可以把球体网格模型沿着y轴进行平移, 方法translateX()、translateZ()表示其它两个轴方向上的平移,方法的参数是平移距离,在网格模型对象的后面添加如下代码,刷新浏览器查看页面效果,如果场景超出显示区域,可以调整照相机参数s的大小。
sphereMesh.translateY(100);//球体网格模型沿Y轴正方向平移100
设置透明度和高光
前面案例中网格模型材质只是设置了一个颜色,实际渲染的时候往往会设置其他的参数,比如实现玻璃效果要设置材质透明度,一些光亮的表面要添加高光效果。 本节课在《同时绘制立方体和球体》代码的基础上进行更改,不用重新编写代码。
设置透明度
更改场景中的球体材质对象构造函数THREE.MeshLambertMaterial()的参数,添加opacity和transparent属性,opacity的值是0~1之间,transparent表示是否开启透明度效果, 默认是false表示透明度设置不起作用,值设置为true,网格模型就会呈现透明的效果,使用下面的代码替换原来的球体网格模型的材质, 刷新浏览器可以看到半透明的球体和立方体颜色叠加融合的效果。
var sphereMaterial=new THREE.MeshLambertMaterial({ color:0xff0000, opacity:0.7, transparent:true });//材质对象
添加高光
处在光照条件下的物体表面会发生光的反射现象,不同的表面粗糙度不同,宏观上来看对光的综合反射效果,可以使用两个反射模型来概括,一个是漫反射,一个是镜面反射, 使用渲染软件或绘画的时候都会提到一个高光的概念,其实说的就是物理光学中镜面反射产生的局部高亮效果。实际生活中的物体都是镜面反射和漫反射同时存在,只是那个占得比例大而已, 比如树皮的表面更多以漫反射为主基本没有体现出镜面反射,比如一辆轿车的外表面在阳光下你会看到局部高亮的效果,这很简单汽车表面经过抛光等表面处理粗糙度非常低, 镜面反射效果明显,对于three.js而言漫反射、镜面反射分别对应两个构造函数MeshLambertMaterial()、MeshPhongMaterial(),通过three.js引擎你可以很容易实现这些光照模型, 不需要自己再使用原生WebGL实现,更多关于光照模型的知识可以参考WebGL教程中的《光照渲染立方体》和计算机图形学的相关书籍。
前面案例都是通过构造函数MeshLambertMaterial()实现漫反射进行渲染,高光效果要通过构造函数MeshPhongMaterial()模拟镜面反射实现,属性specular表示球体网格模型的高光颜色,改颜色的RGB值会与光照颜色的RGB分量相乘, shininess属性可以理解为光照强度的系数,初学的的时候这些细节如果不清楚,不用深究,每个人的基础不同,理解问题的深度和角度不同,比如高光,学习过计算机图形学的会联想到镜面反射模型和物理光学, 从事过与美术相关工作,都知道需要的时候会给一个物体添加高光,视觉效果更加高亮,因此对于构造函数MeshPhongMaterial()的参数设置不太清除也没关系,对于零基础的读者本节课的要求就是有个简单印象就可以, 站在黑箱外面理解黑箱;对于有WebGL基础的,可以思考three.js引擎构造函数实际封装了哪些WebGL API和图形学算法,站在黑箱里面理解黑箱,如果是你你会怎么封装开发一个三维引擎,这样你可以从底层理解上层的问题, 保证学习的连贯性;如果你使用过其它的三维建模渲染软件,那就使用three.js这个黑箱类比一个你熟悉的黑箱,通过类比降低学习难度,比如你可以打开3dmax软件设置一个材质的高光,体验下视觉效果。 直接使用下面的代码替换上面的透明度材质即可,刷新浏览器可以看到球体表面的高光效果。
var sphereMaterial=new THREE.MeshPhongMaterial({ color:0x0000ff, specular:0x4488ee, shininess:12 });//材质对象
添加旋转动画
基于WebGL技术开发在线游戏、商品展示、室内漫游往往都会涉及到动画,初步了解three.js可以做什么,深入讲解three.js动画之前,本节课先制作一个简单的立方体旋转动画。 下面的代码在《第一个3D场景》已绘制好的立方体上进行更改。
在HTML5标准中,W3C提供了一个用于动画的方法requestAnimationFrame(),该方法属于浏览器的window对象可以直接调用,参数是将要被调用函数的函数名,该方法调用函数不是立即调用而是向浏览器发起一个执行某函数的请求, 什么事会执行由浏览器决定,一般默认保持60FPS的频率,大约每16.7ms调用一次equestAnimationFrame()方法指定的函数,60FPS是理想的情况下,如果运行的程序比较多可能会低于这个频率。关注《requestAnimationFrame()》获取更多关于该方法的知识。
在第一节课程中说明过,每执行一次渲染器对象的render()方法,浏览器就会通过CPU把相关的图形数据发送到GPU和显存,然后渲染出一帧图像,这就是说你按照一定的周期调用该方法就可以不停地生成新的图像覆盖原来的图像, 这时要注意我为了产生立方体的旋转动画效果,每执行一次render()渲染方法,要把立方体绕一个坐标轴旋转一定的角度,立方体不停地旋转,相机不停地拍照自然就会形成动画的效果。可以使用下面的代码替换原来第57行的代码renderer.render(scene,camera);。
function render() { renderer.render(scene,camera);//执行渲染操作 mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度 requestAnimationFrame(render);//请求再次执行渲染函数render } render();
代码解析
上面代码定义了一个渲染函数render(),函数中定义了三个语句,通过requestAnimationFrame(render)可以实现循环调用函数render(),每次调用渲染函数的时候,执行 renderer.render(scene,camera);渲染出一帧图像,执行mesh.rotateY(0.01);语句使立方体网格模型绕y轴旋转0.01弧度,理想的情况下每秒执行60次,相当于旋转的角速度是108度每秒。
均匀旋转
在实际执行程序的时候,可能requestAnimationFrame(render)请求的函数并不一定能按照理想的60FPS频率执行,两次执行渲染函数的时间间隔也不一定相同, 如果执行旋转命令的rotateY的时间间隔不同,旋转运动就不均匀,为了解决这个问题需要通过程序记录两次执行绘制函数的时间间隔。
使用下面的渲染函数替换原来的渲染函数即可,rotateY()的参数是0.001*t,也意味着两次调用渲染函数执行渲染操作的间隔t毫秒时间内,立方体旋转了0.001*t弧度,很显然立方体的角速度是0.001弧度每毫秒(0.0001 rad/ms = 1 rad/s = 180度/s)。CPU和GPU执行一条指令时间是纳秒ns级,相比毫秒ms低了6个数量级,所以一般不用考虑渲染函数中几个计时语句占用的时间,除非你编写的是要精确到纳秒ns的级别的标准时钟程序。
let T0 = new Date();//上次时间 function render() { let T1 = new Date();//本次时间 let t = T1-T0;//时间差 T0 = T1;//把本次时间赋值给上次时间 requestAnimationFrame(render); renderer.render(scene,camera);//执行渲染操作 mesh.rotateY(0.001*t);//旋转角速度0.001弧度每毫秒 } render();
鼠标操作三维场景
为了使用鼠标操作三维场景,可以借助three.js众多控件之一OrbitControls.js,可以在下载的three.js-master文件中找到(three.js-master\examples\js\controls)。 然后和引入three.js文件一样在html文件中引入控件OrbitControls.js。
<!--引入three.js三维引擎--> <script src="http://www.yanhuangxueyuan.com/3D/example/three.min.js"></script> <!--引入轨道控件OrbitControls.js--> <script src="OrbitControls.js"></script>
OrbitControls.js控件支持鼠标左中右键操作和键盘方向键操作,具体代码如下,使用下面的代码替换《第一个3D场景》中第57行代码renderer.render(scene,camera);即可。
- 缩放:滚动—鼠标中键
- 平移:拖动—鼠标右键
- 旋转:拖动—鼠标左键
模型操作:
function render() { renderer.render(scene,camera);//执行渲染操作 } render(); var controls = new THREE.OrbitControls(camera);//创建控件对象 controls.addEventListener('change', render);//监听鼠标、键盘事件
OrbitControls.js控件提供了一个构造函数THREE.OrbitControls(),把一个相机对象作为参数的时候,执行代码new THREE.OrbitControls(camera),浏览器会自动检测鼠标键盘的变化, 并根据鼠标和键盘的变化更新相机对象的参数,比如你拖动鼠标左键,浏览器会检测到鼠标事件,把鼠标平移的距离按照一定算法转化为相机的的旋转角度,你可以联系生活即使景物没有变化,你的相机拍摄角度发生了变化,自然渲染器渲染出的结果就变化了,通过定义监听事件controls.addEventListener('change', render),如果你连续操作鼠标,相机的数据不停的变化,同时会不停的调用渲染函数,把更新的相机数据传递给GPU重新绘制出来。
执行构造函数THREE.OrbitControls()浏览器会同时干两件事,一是给浏览器定义了一个鼠标、键盘事件,自动检测鼠标键盘的变化,如果变化了就会自动更新相机的数据, 执行该构造函数同时会返回一个对象,可以给该对象添加一个监听事件,只要鼠标或键盘发生了变化,就会触发渲染函数。 关于监听函数addEventListener可以关注HTML5教程《HTML5事件》的介绍。 如果有webgl基础的话,也可以自己封装各种用途的three.js控件。
方式2
一个场景同时要求立方体旋转动画和鼠标操作场景,鼠标事件触发渲染函数和requestAnimationFrame()请求执行绘制函数会发生冲突, 这时候一般只需要定义requestAnimationFrame()方法,不需要设置轨道控件的监听,这很容易理解, 执行THREE.OrbitControls()构造函数后浏览器就可以做到实时更新相机的数据,然后通过requestAnimationFrame()定义一个主循环,周期性执行渲染函数, 大家都知道每次执行渲染函数,所有的网格模型、灯光、相机数据等数据都会重新发送给GPU渲染出一幅图像,无论相机数据是否更新, 只要重新执行渲染方法render()都会重新传入GPU数据,正是因为每次执行绘制函数都重新给GPU传输数据,这就可以保证更新的相机数据会反映到GPU渲染的结果中。
function render() { renderer.render(scene,camera);//执行渲染操作 requestAnimationFrame(render);//请求再次执行渲染函数render } render(); var controls = new THREE.OrbitControls(camera);//创建控件对象
同时定义旋转动画和鼠标操作
function render() { renderer.render(scene,camera);//执行渲染操作 mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度 requestAnimationFrame(render);//请求再次执行渲染函数render } render(); var controls = new THREE.OrbitControls(camera);//创建控件对象