Three.js学习笔记
Web图形库(Web Graphics Library)简称WebGL,是在浏览器环境下进行3D/2D图像渲染的技术。你不需要额外的插件,就可以在HTML5的Canvas上绘制复杂的、可交互的图形。
大部分现代浏览器支持WebGL技术,IE从11开始支持,老版本的IE可以通过第三方插件支持,例如IEWebGL。
WebGL基于OpenGL ES 2.0提供3D图形接口。后者是OpenGL的一个子集,主要针对手机、PDA之类的嵌入式设备。
直接使用WebGL编程难度较高,需要了解WebGL的细节、学习复杂的着色器(Shader)语言。Three.js对WebGL的底层细节进行了封装,让你更加容易的、仅利用JavaScript语言创建3D图形,你可以:
- 创建简单/复杂的3D几何图形
- 在3D场景中动画、移动对象
- 给对象应用纹理、材质
- 从3D模型软件中加载对象
在本节,我们创建以下几个对象:
对象 | 说明 |
Plane | 平面,二维的矩形,作为“地面”在场景的中央展示 |
Cube | 三维盒子,展示为红色 |
Sphere | 三维球体,展示为蓝色 |
Camera | 镜头,决定你看到的场景是什么样子 |
Axes | X/Y/Z轴,辅助的调试工具,方便查看对象在哪里渲染 |
代码及注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Three.js Study</title> <script src="https://code.jquery.com/jquery-1.12.4.js"></script> <script src="three.js"></script> <style> body { margin: 0; overflow: hidden; } </style> </head> <body> <div id="WebGL"></div> <script type="text/javascript"> $( function () { // 场景 var scene = new THREE.Scene(); var aspect = window.innerWidth / window.innerHeight; /** * 定义一个透视镜头,参数: * fov FOV垂直分量,镜头到视截锥近平面上下边之间的夹角 * aspect 视截锥的纵横比 * near 近平面离镜头多远 * far 远平面离镜头多远 */ var camera = new THREE.PerspectiveCamera( 45, aspect, 0.1, 1000 ); // WebGL渲染器,使用显卡来渲染场景。尽管还存在其它渲染器实现,但是处于性能、特性的考虑,不推荐使用 var renderer = new THREE.WebGLRenderer(); // 设置背景色,第二参数为透明度 renderer.setClearColor( 0xEEEEEE, 1 ); // 设置场景的大小 renderer.setSize( window.innerWidth, window.innerHeight ); // 创建一个调试用途的坐标轴,X轴红色、Y轴绿色、Z轴蓝色。20表示轴线长度 var axes = new THREE.AxisHelper( 20 ); // 把对象添加到场景中 scene.add( axes ); // 创建一个平面几何图形,宽60高20,在宽、高方向上分段数为1(不切分) var planeGeometry = new THREE.PlaneGeometry( 60, 20, 1, 1 ); // 材质,定义颜色、透明度、反光效果之类的属性 // MeshBasicMaterial是一种简单材质,它不受光线影响,使用纯色或者网格(wireframe)渲染几何图形 var planeMaterial = new THREE.MeshBasicMaterial( { color: 0xcccccc } ); // Mesh表示一类基于三角形网格(triangular polygon mesh)的对象 var plane = new THREE.Mesh( planeGeometry, planeMaterial ); // 默认的,平面的对称中心位于原点,width与X轴平行,height与Y轴平行 // 在X轴方向逆时针(从原点往X轴正向看)旋转90度 plane.rotation.x = -0.5 * Math.PI; // 圆周长2PI,PI代表180度, // 在X轴方向偏移15 plane.position.x = 15; plane.position.y = 0; plane.position.z = 0; scene.add( plane ); // 类似的,创建一个立方体,类似的,其对称中心也是默认位于原点 var cubeGeometry = new THREE.CubeGeometry( 4, 4, 4 ); // wireframe表示绘制网格线 var cubeMaterial = new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: true } ); var cube = new THREE.Mesh( cubeGeometry, cubeMaterial ); cube.position.x = -4; cube.position.y = 3; cube.position.z = 0; scene.add( cube ); // 绘制一个球体 var sphereGeometry = new THREE.SphereGeometry( 4, 20, 20 ); var sphereMaterial = new THREE.MeshBasicMaterial( { color: 0x7777ff, wireframe: true } ); var sphere = new THREE.Mesh( sphereGeometry, sphereMaterial ); sphere.position.x = 20; sphere.position.y = 4; sphere.position.z = 2; scene.add( sphere ); // 移动镜头 camera.position.x = -30; camera.position.y = 40; camera.position.z = 30; // 将镜头指向场景的中心 camera.lookAt( scene.position ); // 渲染 $( "#WebGL" ).append( renderer.domElement ); renderer.render( scene, camera ); } ); </script> </body> </html> |
渲染效果:
本节我们改进一下上面的例子,修改材质,并添加光线、阴影效果。
首先,为场景添加一个光源:
1 2 3 4 5 |
// 聚光灯效果的白色光源 var spotLight = new THREE.SpotLight( 0xffffff ); // 设置聚光灯的位置 spotLight.position.set( -40, 60, -10 ); scene.add( spotLight ); |
添加这段代码后,渲染效果不会有任何改变。原因我们已经在前面的代码注释中提到过, MeshBasicMaterial这种材质不会对光线作出反应。我们替换一下材质:
1 2 3 4 5 |
var planeMaterial = new THREE.MeshLambertMaterial( { color: 0xcccccc } ); // ... var cubeMaterial = new THREE.MeshLambertMaterial( { color: 0xff0000 } ); // ... var sphereMaterial = new THREE.MeshLambertMaterial( { color: 0x7777ff} ); |
除了MeshLambertMaterial之外,MeshPhongMaterial也会对光源作出反应。
现在刷新一下页面,可以看到如下渲染效果:
比上一幅截图好看多了,但是还有点不自然,因为没有阴影效果。
由于渲染阴影比较消耗资源,因此默认情况下Three.js关闭了阴影。要启用阴影其实很简单:
1 2 |
// 启用阴影效果 renderer.shadowMapEnabled = true; |
此外,你还需要定义什么对象产生(cast)阴影,什么对象接收阴影:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 阴影由平面接收 plane.receiveShadow = true; // ... cube.castShadow = true; // ... sphere.castShadow = true; // 设置产生阴影的光源 spotLight.castShadow = true; // 提高阴影质量 spotLight.shadowMapWidth = spotLight.shadowMapHeight = 1024 * 4; |
要想为场景添加动画效果,我们需要找到定期重渲染场景的方法。setInterval()这种定时器是不适合的,因为它与渲染行为不是同步的,会导致严重性能问题。
requestAnimationFrame() 是现代浏览器支持的、避免两setInterval()缺点的函数。你可以为它提供一个回调,此回调会定期(间隔由浏览器定义)的被调用。在回调中你可以指定任何渲染逻辑,浏览器负责尽可能平滑、高效的绘制。示例代码:
1 2 3 4 |
function renderScene() { requestAnimationFrame( renderScene ); renderer.render( scene, camera ); } |
上面的函数把自己传递给requestAnimationFrame,从而导致函数的逻辑被反复调用,从而可以产生动画效果。
为了显示动画帧率信息,我们引入一个助手库stats.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<script src="stat.js"></script> <script type="text/javascript"> var stats = new Stats(); stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom document.body.appendChild( stats.dom ); function renderScene() { // 开始统计 stats.begin(); // 这里编写被监控的代码 stats.end(); requestAnimationFrame( animate ); } requestAnimationFrame( renderScene ); </script> |
下面我们为立方体添加一个翻滚效果,为球体添加一个弹跳效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var step = 0; function animate() { stats.begin(); cube.rotation.x += 0.02; cube.rotation.y += 0.02; cube.rotation.z += 0.02; step += 0.04; // 定义弹跳速度 sphere.position.x = 20 + ( 10 * (Math.cos( step ))); sphere.position.y = 2 + ( 10 * Math.abs( Math.sin( step ) )); renderer.render( scene, camera ); // 反复渲染 stats.end(); requestAnimationFrame( animate ); } requestAnimationFrame( animate ); |
刷新浏览器,可以查看动画效果,注意左上角的帧率窗口。
上一章的学习中,我们创建了由几个对象构成的简单场景,并制作了简单的动画效果。现在我们来更深入的了解一下构成Three.js场景的组件。
之前我们调用 new THREE.Scene() 创建了一个场景。场景是一个容器,它内部可以包含三类东西:
- 镜头(Camera):决定了查看场景的角度和方式。在渲染场景的时候镜头可以自动创建,但是你也可以手工指定其参数
- 光源(Lights):影响材质的渲染效果、阴影
- 物体(Objects):场景中渲染的主要东西。包括各种几何形状、导入的模型
属性/方法 | 说明 | ||
children | 所有对象组成的数组 | ||
getChildByName(name) | 根据名称来查找对象 | ||
remove(obj) | 从场景中移除一个对象 | ||
traverse(callback) | 指定一个回调,针对场景中所有对象调用之 | ||
fog | 添加烟雾效果,这样越远的物体显示越模糊:
|
||
overrideMaterial | 覆盖场景中所有物体的材质设置:
|
Three.js提供了大量开箱即用的Geometry, Geometry用于定义物体的形状,材质则定义其外观。
在大部分3D图形库中,Geometry基本上都是三维空间中一系列点、以及连接这些点的面的集合。以立方体为例:
- 每个立方体包含8个角,这些角可以由三维空间中的一个点来确定。这些点称为顶点(vertices)
- 每个立方体包含6个面,这些面的每个角都对应一个顶点。这些面称为face
当使用Three.js自带的Geometry时你不需要逐个定义所有点、面。例如对于Cube,你只需要定义长宽高即可,Three.js会利用你提供的这些信息创建所有必须的点、面。
Three.js允许自定义点、面,然后构成一个几何图形。下面是手工构建Cube的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// 所有顶点 var vertices = [ new THREE.Vector3( 1, 3, 1 ), new THREE.Vector3( 1, 3, -1 ), new THREE.Vector3( 1, -1, 1 ), new THREE.Vector3( 1, -1, -1 ), new THREE.Vector3( -1, 3, -1 ), new THREE.Vector3( -1, 3, 1 ), new THREE.Vector3( -1, -1, -1 ), new THREE.Vector3( -1, -1, 1 ) ]; // 所有三角形的面,数字为从0开始的顶点序号 var faces = [ new THREE.Face3( 0, 2, 1 ), new THREE.Face3( 2, 3, 1 ), new THREE.Face3( 4, 6, 5 ), new THREE.Face3( 6, 7, 5 ), new THREE.Face3( 4, 5, 1 ), new THREE.Face3( 5, 0, 1 ), new THREE.Face3( 7, 6, 2 ), new THREE.Face3( 6, 3, 2 ), new THREE.Face3( 5, 7, 0 ), new THREE.Face3( 7, 2, 0 ), new THREE.Face3( 1, 3, 4 ), new THREE.Face3( 3, 6, 4 ), ]; var geom = new THREE.Geometry(); geom.vertices = vertices; geom.faces = faces; // 在重新设置顶点数组后,提示需要更新。这是因为Three.js默认假设Mesh的Geometry的形状在生命周期内保持不变 geom.verticesNeedUpdate = true; // 根据顶点重新计算面法线 geom.computeFaceNormals(); |
在以前版本的Three.js中,允许使用四边形来定义面。四边形在建模时比较受欢迎,原因是很容易被增强、平滑。三角形在渲染、游戏引擎中比较受欢迎,原因是比较简单。
有了Geometry后,加上材质就可以构成简单的3D物体——Mesh了:
1 2 3 4 5 6 7 8 |
// 材质数组 var materials = [ new THREE.MeshLambertMaterial( { opacity: 0.6, color: 0x44ff44, transparent: true } ), new THREE.MeshBasicMaterial( { color: 0x666666, wireframe: true } ) ]; // Mesh组 var mesh = THREE.SceneUtils.createMultiMaterialObject( geom, materials ); scene.add( mesh ); |
Three.js允许给Geometry应用多个材质,上例中的Cube既有颜色填充,也显示了线条,这是两种材质的混合效果。从实现角度来说,Three.js创建了两个THREE.Mesh实例,每个材质对应一个实例,这两个实例被放置到一个组里面。添加组到场景的方式,与添加Mesh一致。
我们可以调用组的forEach,对其中所有Mesh进行操作:
1 2 3 |
mesh.children.forEach( function ( e ) { e.castShadow = true } ); |
属性/方法 | 说明 |
vertices | 构成此Geometry的顶点坐标数组 |
faces | 构成Geometry的三角形面数组 |
verticesNeedUpdate | 修改顶点数组后,提示Three.js需要更新顶点 |
computeFaceNormals() | 重新根据顶点来计算面 |
clone() | 克隆一个Geometry |
属性/方法 | 说明 | ||
position.x|y|z position.set(x,y,z) |
此物体相对于父对象的位置,大部分物体的父对象是THREE.Scene对象,对于组中的Mesh,其父对象是组。示例代码:
|
||
rotation.x|y|z | 让物体围绕自己的轴(而不是场景的)旋转一定角度。与position类似,具有三种设置方法 | ||
scale.x|y|z | 让物体在其轴方向缩放。与position类似,具有三种设置方法 | ||
translateX(amount) |
将物体沿着X/Y/Z轴方向移动 这些方法指定的是相对位移,而position指定的是绝对值 |
||
translateY(amount) | |||
translateZ(amount) |
Three.js支持两种类型的镜头:正交(orthographic)镜头、透视(perspective)镜头。到目前为止我们还没有使用过正交镜头。
正交镜头的特点是,物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体,其大小不会变化。正交镜头适合2D游戏。
透视镜头则是模拟人眼的视觉特点,距离远的物体显得更小。透视镜头通常更适合3D渲染。
构造函数参数:
参数 | 说明 |
fov |
视界,从镜头可以看到的场景的部分。其值为镜头到近平面上下边的夹角 人眼的FOV接近180度,某些鸟类的FOV打到360度。但是计算机屏幕做不到覆盖视野,通常3D游戏的FOV取值在60-90度之间 较好的默认值为45 |
aspect | 渲染区域的纵横比。较好的默认值为window.innerWidth/window.innerHeight |
near | 近平面离镜头的距离。较好的默认值为0.1 |
far | 远平面离镜头的距离。较好的默认值为1000 |
关于这些参数的形象化描述,请参考术语视截锥中的截图。
正交镜头不关心FOV、纵横比这些概念。其构造函数实际上是指定了一个Cube,落在其中的物体会被渲染:
参数 | 说明 |
left | 相机截锥左平面位置,如果你将其设置为-100,则位置在其更左边的物体将不可见 |
right | 相机截锥右平面位置 |
top | 相机截锥上平面位置 |
bottom | 相机截锥下平面位置 |
near | 近平面的位置 |
far | 远平面的位置 |
关于这些参数的形象化描述,参考下图:
创建镜头后,还需要将其移动、然后对准物体积聚的场景中心位置,才能确保物体的渲染。移动镜头,通过设置其position属性来实现:
1 2 3 |
camera.position.x = 120; camera.position.y = 60; camera.position.z = 180; |
聚焦,则是调用下面的方法实现:
1 |
camera.lookAt( new THREE.Vector3( x, 10, 0 ) ); |
所谓HUD(head-up display,平视显示),是指在屏幕(挡风玻璃)上显示一些辅助信息(例如飞机、汽车的仪表信息),避免驾驶员低头分散注意力。HUD的特点是其显示内容的大小、位置与镜头无关。
要实现HUD效果,可以同时渲染两套场景,其中HUD场景使用OrthographicCamera镜头:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var scene = new THREE.Scene(); var sceneOrtho = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 250); // 正交镜头的近平面的大小,和浏览器窗口大小一致 var cameraOrtho = new THREE.OrthographicCamera(0, window.innerWidth, window.innerHeight, 0, -10, 10); var webGLRenderer = new THREE.WebGLRenderer(); webGLRenderer.render(scene, camera); // 防止在下一次render时,自动清屏 webGLRenderer.autoClear = false; webGLRenderer.render(sceneOrtho, cameraOrtho); |
在Three.js中光源很重要。不设置光源你就看不到被渲染的物体。Three.js内置了多种光源以满足特定场景的需要。
光源 | 说明 |
AmbientLight | 环境光,其颜色均匀的应用到场景及其所有对象上 |
PointLight | 3D空间中的一个点光源,向所有方向发出光线 |
SpotLight | 产生圆锥形光柱的聚光灯,台灯、天花板射灯通常都属于这类光源 |
DirectionalLight | 也就无限光,光线是平行的。典型的例子是日光 |
HemisphereLight | 特殊光源,用于创建户外自然的光线效果,此光源模拟物体表面反光效果、微弱发光的天空 |
AreaLight | 面光源,指定一个发光的区域 |
LensFlare | 不是光源,用于给光源添加镜头光晕效果 |
这种光源为场景添加全局的环境光。这种光没有特定的方向,不会产生阴影。通常不会把AmbientLight作为唯一的光源,而是和SpotLight、DirectionalLight等光源结合使用,从而达到柔化阴影、添加全局色调的效果。
指定颜色时要相对保守,例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和,什么都看不清:
1 2 3 |
var ambiColor = "#0c0c0c"; var ambientLight = new THREE.AmbientLight(ambiColor); scene.add(ambientLight); |
该类模拟一个点光源,具有以下属性:
属性 | 说明 |
color | 光线的颜色 |
intensity | 光线的强度,默认1,浮点数 |
distance | 光线能照耀的距离 |
position | 光源的位置 |
visible | 设置为true则打开光源 |
示例代码:
1 2 3 4 5 6 |
var pointColor = "#ccffcc"; var pointLight = new THREE.PointLight( pointColor ); pointLight.distance = 100; scene.add( pointLight ); // 设置强度 pointLight.intensity = 2.4; |
这种光源的使用场景最多,特别是在你需要阴影效果的时候。PointLight的所有属性对于SpotLight可用,前者还包括以下属性:
属性 | 说明 | ||
castShadow |
此光源是否可以导致物体产生阴影 注意:目标物体需要设置receiveShadow |
||
shadowCameraNear | 从距离光源多远的地方开始创建阴影 | ||
shadowCameraFar | 到距离光源多远的地方不再创建阴影 | ||
shadowCameraFov | 阴影的FOV | ||
target | 此光源指向的目标。光线从光源照向该目标:
|
||
shadowBias | 设置阴影的位置偏移 | ||
angle | 光锥的夹角,默认Math.PI/3 | ||
exponent | 衰减指数,即随着与光源距离的增加,光线衰减的速度 | ||
onlyShadow | 如果设置为true,仅仅产生阴影,而不照亮物体 | ||
shadowCameraVisible | 如果设置为true,你将看到光源如何、从何处产生阴影(显示截锥)。用于调试目的 | ||
shadowDarkness | 阴影的暗度,默认0.5。一旦场景被创建此参数即不可修改 | ||
shadowMapWidth shadowMapHeight |
有多少像素用于创建阴影,如果阴影出现锯齿效果,可以增加此参数。一旦场景被创建此参数即不可修改 另一种减轻阴影锯齿的方法是,让阴影相机截锥尽可能小 |
示例代码:
1 2 3 4 5 6 7 8 9 10 11 |
var spotLight = new THREE.SpotLight(pointColor); spotLight.position.set(-40, 60, -10); spotLight.castShadow = true; spotLight.shadowCameraNear = 2; spotLight.shadowCameraFar = 200; spotLight.shadowCameraFov = 30; spotLight.target = plane; // 跟踪目标 spotLight.distance = 0; spotLight.angle = 0.4; scene.add(spotLight); |
光锥的宽、高可以基于以下代码求出:
1 2 |
var coneLength = light.distance ? light.distance : 10000; var coneWidth = coneLength * Math.tan( light.angle * 0.5 ) * 2; |
用于模拟遥远的,类似太阳那样的光源。该光源与SpotLight的主要区别是,它不会随着距离而变暗,所有被照耀的地方获得相同的光照强度。
DirectionalLight具有大部分SpotLight的属性。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var directionalLight = new THREE.DirectionalLight( pointColor ); directionalLight.position.set( -40, 60, -10 ); directionalLight.castShadow = true; directionalLight.shadowCameraNear = 2; directionalLight.shadowCameraFar = 200; directionalLight.shadowCameraLeft = -50; directionalLight.shadowCameraRight = 50; directionalLight.shadowCameraTop = 50; directionalLight.shadowCameraBottom = -50; directionalLight.distance = 0; directionalLight.intensity = 0.5; directionalLight.shadowMapHeight = 1024; directionalLight.shadowMapWidth = 1024; scene.add( directionalLight ); |
模拟穹顶(半球)的微弱发光效果,让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线,但是不够真实,因为无法体现大气层的散射效果、地面或物体的反射效果。常用属性:
属性 | 说明 |
color | 天空散射的光线颜色 |
groundColor | 地面散射的光线颜色 |
intensity | 光线强度 |
示例代码:
1 2 3 4 |
// 三个参数分别对应天空颜色、地面颜色、强度 var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6); hemiLight.position.set(0, 500, 0); scene.add(hemiLight); |
用于定义一个发光的矩形区域,该光源属于Three.js扩展。
THREE.WebGLRenderer这个渲染器不能和AreaLight一起使用,原因是THREE.AreaLight是一种复杂的光源,与WebGLRenderer一起使用会导致严重的性能问题。
渲染器THREE.WebGLDeferredRenderer使用不同的途径来渲染场景,它将渲染拆分为几个步骤。它能够处理复杂的光源或者数量众多的光源。
通过前面章节的学习,我们已经知道材质 + Geometry可以构成Mesh——可以添加到3D场景中的物体。
Geometry就好像是骨架,材质则类似于皮肤,它定义了Geometry的外观——是否有金属质感、是否透明、是否显示为线框(wireframe)。
材质 | 说明 |
MeshBasicMaterial | 基本的材质,显示为简单的颜色或者显示为线框。不考虑光线的影响 |
MeshDepthMaterial | 使用简单的颜色,但是颜色深度和距离相机的远近有关 |
MeshNormalMaterial | 基于面Geometry的法线(normals)数组来给面着色 |
MeshFacematerial | 容器,允许为Geometry的每一个面指定一个材质 |
MeshLambertMaterial | 考虑光线的影响,哑光材质 |
MeshPhongMaterial | 考虑光线的影响,光泽材质 |
ShaderMaterial | 允许使用自己的着色器来控制顶点如何被放置、像素如何被着色 |
LineBasicMaterial | 用于THREE.Line对象,创建彩色线条 |
LineDashMaterial | 用于THREE.Line对象,创建虚线条 |
RawShaderMaterial | 仅和THREE.BufferedGeometry联用,优化静态Geometry(顶点、面不变)的渲染 |
SpriteCanvasMaterial | 在针对单独的点进行渲染时用到 |
SpriteMaterial | |
PointCloudMaterial |
作为所有材质的基类,THREE.Material提供了以下属性:
属性 | 说明 |
基本属性 最常用的属性,用于控制对象的透明度、是否可见、如何被引用(基于ID还是名称) |
|
id | 当你创建一个材质的时候自动分配,作为材质实例的标识,从0开始自动计数 |
uuid | 全局唯一标识,内部使用 |
name | 给材质分配一个名称,调试用 |
opacity |
设定材质的透明度,和transparent联用,范围0~1 |
transparent |
如果设置为true,则Three.js考虑opacity的设置。对于具有Alpha通道的纹理,该属性也需要设置为true |
overdraw | 使用THREE.CanvasRenderer渲染器时多边形会比预期的绘制的大一些。如果使用该渲染器时你法线Gaps可设置为true |
visible | 材质是否可见,如果设置为false则物体看不见 |
side |
材质应用到目标Geomotry的哪一面。默认THREE.Frontside表示应用在外面,可选值THREE.BackSide应用在里面、THREE.DoubleSide应用到两面 对于不封闭空间的Geomotry,例如平面,此属性重要 |
needsUpdate | 改变材质的某些属性后,你可以设置该属性为true,这样Three.js就会丢弃缓存,重新渲染材质 |
混合(Blending)属性 定义对象如何与其背景混合,或者说我们渲染的颜色如何与其背后的颜色交互 |
|
blending | 决定材质如何与背景混合,默认值THREE.NormalBlending,表示仅仅显示顶层颜色 |
blendsrc | 定义物体(源)如何混合到背景(目标)中,默认THREE.SrcAlphaFactor表示基于物体的Alpha通道进行混合 |
blenddst | 定义在混合时,背景(目标)如何渲染,默认THREE.OneMinusSrcAlphaFactor表示基于物体的Alpha通道进行混合 |
blendequation | 定义blendsrc、blenddst如何使用,默认将它们相加(AddEquation) |
高级属性 控制低级别的WebGL上下文如何渲染对象,大部分情况下不需要使用 |
|
depthTest |
如果关闭depthTest,意味着同时关闭reading/testing/writing 到底什么是深度测试(depthTest)呢?假设由两个完全一样的形状,位于你的正前方。真实世界中,你仅能看到里你近的那一个。但是在3D渲染过程中:
所谓深度测试,是现代GPU中内置的一个工具,能够让渲染输出总是符合预期,而不管对象的输出先后顺序。具体实现机制是:当绘制一个像素时,会查看此像素位置原先的depth(即离相机的远近)值,如果新的像素depth值较小,则执行绘制,否则保留原来的值 由于深度测试的实现机制,和透明度(混合)在一起工作时可能出现问题,有时候需要禁用 |
本章跳过了所有和纹理(textures)、映射(maps)、动画 有关的属性。
你可以把属性组成一个对象,作为构造函数的入参:
1 2 3 4 5 6 |
var material = new THREE.MeshBasicMaterial( { color: 0xff0000, name: 'material-1', opacity: 0.5, transparency: true } ); |
或者逐个的设置属性:
1 2 3 4 5 |
var material = new THREE.MeshBasicMaterial(); material.color = new THREE.Color( 0xff0000 ); // 这种方式必须提供Color对象 material.name = 'material-1'; material.opacity = 0.5; material.transparency = true; |
该材质不考虑场景中的光源,目标物体被渲染成简单的、扁平(Flat)的形状。可选的,你可以显示物体的线框(Wireframe),线框由所有面的边构成。
该材质具有以下额外属性:
属性 | 说明 |
color | 材质的颜色 |
wireframe | 是否显示线框。显示线框对于调试有帮助 |
Wireframelinewidth | 线框的线条宽度 |
shading |
定义如何着色,可选值THREE.SmoothShading、THREE.NoShading、THREE.FlatShadin 默认值THREE.SmoothShading,导致渲染平滑的渲染——例如平滑过渡颜色 |
vertexColors | 可以定义各个顶点的颜色,默认值THREE.NoColors。你可以设置为THREE.VertexColors,这样渲染器会考虑Geometry.colors属性 |
fog | 该材质是否被全局迷雾效果影响 |
使用这种材质,物体的外观会受到物体离镜头的距离的影响——随着距离增加而淡出。你可以联合使用其它材质,产生淡出效果。该材质具有以下额外属性:
属性 | 说明 |
wireframe | 是否显示线框 |
wireframeLineWidth | 线框的宽度 |
设置相机的near、far属性,可以决定使用此材质的物体的淡出速度 。如果far - near很大,则物体的淡出速度非常慢。
每个面显示特定的颜色,其颜色取决于该面的法线(垂直于面的向量)。当物体旋转时,其固定角度的颜色保持不变。
法线在Three.js中被大量使用,它被用来确定光线反射效果、帮助映射纹理到3D模型,并且为如何照亮、shade、染色(color)一个表面上的像素点。
为了查看法线的方向,我们可以使用THREE.ArrowHelper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//遍历球体的所有面 for ( var f = 0, fl = sphere.geometry.faces.length; f < fl; f++ ) { var face = sphere.geometry.faces[ f ]; // 计算面的中心点:把面的3个顶点依次加到三维向量中,然后除以3 var centroid = new THREE.Vector3( 0, 0, 0 ); centroid.add( sphere.geometry.vertices[ face.a ] ); centroid.add( sphere.geometry.vertices[ face.b ] ); centroid.add( sphere.geometry.vertices[ face.c ] ); centroid.divideScalar( 3 ); // 创建一个箭头助手 var arrow = new THREE.ArrowHelper( face.normal, // 法线矢量(箭头方向) centroid, // 中心点 (箭头起点) 2, // 长度 0x3333FF, // 颜色 0.5, //箭头长度 0.5 //箭头宽度 ); sphere.add( arrow ); } |
该材质的额外属性包括:wireframe、wireframeLineWidth、shading。 使用FlatShading、SmoothShading的效果分别如下图:
这不是一个单独的材质,而是一个容器。使用它,你可以为每个面指定材质。例如,对于具有12个面(Three.js仅支持三角形面)的Cube,你可以指定具有12个元素的MeshFaceMaterial:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var mats = []; mats.push(new THREE.MeshBasicMaterial({color: 0x009e60})); mats.push(new THREE.MeshBasicMaterial({color: 0x009e60})); mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); mats.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); mats.push(new THREE.MeshBasicMaterial({color: 0xffd500})); mats.push(new THREE.MeshBasicMaterial({color: 0xffd500})); mats.push(new THREE.MeshBasicMaterial({color: 0xff5800})); mats.push(new THREE.MeshBasicMaterial({color: 0xff5800})); mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A})); mats.push(new THREE.MeshBasicMaterial({color: 0xC41E3A})); mats.push(new THREE.MeshBasicMaterial({color: 0xffffff})); mats.push(new THREE.MeshBasicMaterial({color: 0xffffff})); var faceMaterial = new THREE.MeshFaceMaterial(mats); var cubeGeom = new THREE.BoxGeometry( 2.9, 2.9, 2.9 ); var cube = new THREE.Mesh( cubeGeom, faceMaterial ); |
你可以设置 geometry.faces[*].materialIndex 来指名某个面使用MeshFaceMaterial中的哪个元素来渲染。
像MeshDepthMaterial这样的材质,不能设置颜色或者纹理,基本不能单独使用。
Three.js允许联合使用多个材质,以产生新的特效。材质联合也使混合(blending)有意义。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var cubeMaterial = new THREE.MeshDepthMaterial(); var colorMaterial = new THREE.MeshBasicMaterial( { // 绿色材质 color: 0x00ff00, // 允许透明度 transparent: true, // 决定如何与背景(即使用了MeshDepthMaterial的那个内部盒子)进行交互 // MultiplyBlending将前景、背景色进行乘积运算(正片叠底) blending: THREE.MultiplyBlending } ); // 创建两个物体构成的组 var cube = new THREE.SceneUtils.createMultiMaterialObject( cubeGeometry, [ colorMaterial, cubeMaterial ] ); // 避免两个相同大小的重叠物体产生闪烁 cube.children[ 1 ].scale.set( 0.99, 0.99, 0.99 ); |
此材质用于创建哑光效果。提供额外属性:
属性 | 说明 |
ambient | 材质的阴影色,与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合(正片叠底),默认白色 |
emissive | 材质发出的光线的颜色,注意这不会让材质成为光源,只是一个不会被其它光源影响的颜色而已,默认黑色 |
wrapAround |
设置为true则启用半环境光(half-lambert lighting)技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域,设置为true可以柔化阴影、更均匀的分散(distribute)光线 |
wrapRGB | 当wrapAround设置为true时,使用一个THREE.Vector3来控制光线减弱(drop off)的速度,可以用来微调物体的色泽 |
此材质用于创建高反光效果。提供额外属性:
属性 | 说明 |
ambient | 材质的阴影色,与AmbientLight配合。AmbientLight的颜色与该颜色进行乘积混合(正片叠底),默认白色 |
emissive | 材质发出的光线的颜色,注意这不会让材质成为光源,只是一个不会被其它光源影响的颜色而已,默认黑色 |
specular |
材质的高光色,即反光的颜色。如果将其设置:
|
shininess | 高光色的亮度,默认30 |
metal | 设置为true,则Three.js更改算法,让材质更加像金属 |
wrapAround |
设置为true则启用半环境光(half-lambert lighting)技术——光线的减弱行为更加微妙。如果你的Mesh具有尖锐、黑暗的区域,设置为true可以柔化阴影、更均匀的分散(distribute)光线 |
wrapRGB | 当wrapAround设置为true时,使用一个THREE.Vector3来控制光线减弱(drop off)的速度,可以用来微调物体的色泽 |
基于这种材质,可以应用自己开发的着色器。通过定制着色器,你可以精确的定义物体如何被渲染,或者修改Threee.js的默认渲染行为。
ShaderMaterial支持wireframe、Wireframelinewidth、linewidth、shading、vertexColors、fog以及以下额外属性:
属性 | 说明 |
fragmentShader |
使用的片断着色器程序的名称 片断着色器,也叫像素着色器(pixel shader)。用于定义顶点之间每个点如何渲染 |
vertexShader |
使用的顶点着色器程序的名称 顶点着色器,可以操控顶点的属性(例如改变顶点位置) 如果你像让多边形为全红色,可以基于此着色器,指定所有顶点为红色(此颜色信息会传递给片断着色器)。反之,如果你想在顶点之间产生渐变效果,则需要基于片断着色器 顶点着色器位于图形管线(graphic pipeline)的早期,在模型坐标转换、多边形修剪(clipping)之前,此时实际渲染工作并为开始 |
uniforms | 用于向着色器程序发送信息,相同的信息被传递给每个vertex、fragment |
defines | 转换为#define代码片断,设置一些全局变量供着色器程序使用 |
attributes | 用于传递位置性的、法线相关的信息。如果使用该属性,必须为每个顶点提供 |
lights | 是否把光照数据传入着色器,默认false |
对于前面已经讨论过的其它材质,Three.js已经提供了它们的片断着色器、顶点着色器。
着色器不是基于JavaScript语言编写的,它的专用语言是GSGL,即OpenGL ES着色器语言的WebGL支持。这种语言的语法风格类似于C语言。
在本节,我们编写:
- 一个简单顶点着色器。该着色器能够修改Cube顶点的坐标值
- 多个借用自glslsandbox代码的片断着色器,创建具有动画效果的材质
顶点着色器代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<script id="vertex-shader" type="x-shader/x-vertex"> // 外部传入的时间 uniform float time; varying vec2 vUv; void main() { // 计算变换后的位置 vec3 posChanged = position; posChanged.x = posChanged.x*(abs(sin(time*1.0))); posChanged.y = posChanged.y*(abs(cos(time*1.0))); posChanged.z = posChanged.z*(abs(sin(time*1.0))); gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0); } </script> |
为了JavaScript与着色器之间的通信,我们使用所谓uniforms。上面的例子中定义了一个uniform,传递外部的时间,根据此时间来变换顶点的位置。
gl_Position 是一个特殊变量,用于将顶点位置信息传回JavaScript。
其中一个片断着色器代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<script id="fragment-shader-6" type="x-shader/x-fragment"> uniform float time; uniform vec2 resolution; void main( void ) { vec2 uPos = ( gl_FragCoord.xy / resolution.xy ); uPos.x -= 1.0; uPos.y -= 0.5; vec3 color = vec3(0.0); float vertColor = 2.0; for( float i = 0.0; i < 15.0; ++i ) { float t = time * (0.9); uPos.y += sin( uPos.x*i + t+i/2.0 ) * 0.1; float fTemp = abs(1.0 / uPos.y / 100.0); vertColor += fTemp; color += vec3( fTemp*(10.0-i)/10.0, fTemp*i/10.0, pow(fTemp,1.5)*1.5 ); } vec4 color_final = vec4(color, 1.0); // 把颜色传递回JavaScript gl_FragColor = color_final; } </script> |
材质的创建,可以基于以下助手函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function createMaterial( vertexShader, fragmentShader ) { // 从HTML标签中读取着色器源码vertexShader、fragmentShader为脚本标签的ID var vertShader = document.getElementById( vertexShader ).innerHTML; var fragShader = document.getElementById( fragmentShader ).innerHTML; var attributes = {}; // 向着色器传递变量 var uniforms = { time: { type: 'f', value: 0.2 }, scale: { type: 'f', value: 0.2 }, alpha: { type: 'f', value: 0.6 }, resolution: { type: "v2", value: new THREE.Vector2() } }; uniforms.resolution.value.x = window.innerWidth; uniforms.resolution.value.y = window.innerHeight; // 创建一个ShaderMaterial材质 var meshMaterial = new THREE.ShaderMaterial( { uniforms: uniforms, attributes: attributes, vertexShader: vertShader, fragmentShader: fragShader, transparent: true } ); return meshMaterial; } |
渲染循环中,我们需要改变uniform,从而导致着色器绘制结果发生变化,进而产生动画效果:
1 2 3 4 5 6 7 8 9 |
function render() { // 递增time cube.material.materials.forEach( function ( e ) { e.uniforms.time.value += 0.01; } ); requestAnimationFrame( render ); renderer.render( scene, camera ); } |
有两类仅仅支持用在线条(THREE.Line)的材质。线条这种特殊的Geometry仅仅具有顶点,而没有面。
这种线条非常简单,可用属性:
属性 | 说明 |
color | 线条的颜色 |
linewidth | 线条的宽度 |
vertexColors | 设置各个顶点的颜色为THREE.VertexColors类型。覆盖color属性 |
fog | 是否受到全局迷雾的影响 |
除了上面的四个属性以外,还具有以下额外属性:
属性 | 说明 |
scale | 虚线条、线条间隔的缩放比例 |
dashSize | 虚线条的大小 |
gapSize | 线条间隔的大小 |
Three.js内置了大量的Geometry,可以开箱即用。 本章介绍其中的二维、三维Geometry,线条类不再介绍。
二维图形的初始摆放位置是X-Y平面,但是很多情况下需要需要将它们(特别是PlaneGeometry)放置到“地面”上,也就是X-Z平面上。此时可以让它绕着X轴逆时针旋转90度:
1 |
mesh.rotation.x =- Math.PI/2; |
外观上是一个矩形。示例:
1 |
new THREE.PlaneGeometry(width, height, widthSegments, heightSegments); |
可用属性:
属性 | 必 | 说明 |
width | Y | 矩形的宽度 |
height | Y | 矩形的高度 |
widthSegments | N | 宽方向上分段的数量,默认1 |
heightSegments | N | 高方向上分段的数量,默认1 |
外观上是一个圆形或者扇形。示例:
1 2 3 4 |
// 半径为3的圆 new THREE.CircleGeometry(3, 12); // 半径为3的半圆 new THREE.CircleGeometry(3, 12, 0, Math.PI); |
可用属性:
属性 | 必 | 说明 |
radius | N | 圆的半径,默认50 |
segments | N | 分段数,定义了构成圆的面的数量,最小值3,默认值8。更大的面数意味着更平滑的边缘 |
thetaStart | N | 从什么角度绘制起始扇边,默认0,支持范围0 ~ 2 * PI |
thetaLength | N | 从什么角度绘制终止扇边,默认 2 * PI,支持范围0 ~ 2 * PI |
外观上是一个圆环或者扇环。示例:
1 |
Var ring = new THREE.RingGeometry(); |
可用属性:
属性 | 必 | 说明 |
innerRadius | N | 内半径,默认0 |
outerRadius | N | 外半径,默认50 |
thetaSegments | N | 分段数,定义了构成环的面的数量。影响圆弧的平滑度 |
phiSegments | N | 不影响圆环的平滑度,但是可以增加其构成面的数量 |
thetaStart | N | 从什么角度绘制起始扇边,默认0,支持范围0 ~ 2 * PI |
thetaLength | N | 从什么角度绘制终止扇边,默认 2 * PI,支持范围0 ~ 2 * PI |
该形状允许你创建自定义的二维图形,其操作方式类似于SVG/Canvas中的画布。 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
var shape = new THREE.Shape(); // 移动画笔到指定的点 shape.moveTo( 10, 10 ); // 向Y方向画30像素的线段 shape.lineTo( 10, 40 ); // 绘制贝塞尔曲线 shape.bezierCurveTo( 15, 25, 25, 25, 30, 40 ); // 绘制拟合曲线 shape.splineThru( [ new THREE.Vector2( 32, 30 ), new THREE.Vector2( 28, 20 ), new THREE.Vector2( 30, 10 ), ] ); // 绘制二次方曲线 shape.quadraticCurveTo( 20, 15, 10, 10 ); // 添加一个路径到形状中,挖洞 var hole1 = new THREE.Path(); hole1.absellipse( 16, 24, 2, 3, 0, Math.PI * 2, true ); shape.holes.push( hole1 ); // 再挖一个洞 var hole2 = new THREE.Path(); hole2.absellipse( 23, 24, 2, 3, 0, Math.PI * 2, true ); shape.holes.push( hole2 ); // 再一个挖洞 var hole3 = new THREE.Path(); hole3.absarc( 20, 16, 2, 0, Math.PI, true ); shape.holes.push( hole3 ); // 基于上述形状创建Geometry对象 new THREE.ShapeGeometry( shape ); |
运行结果示意图:
ShapeGeometry支持以下属性:
属性 | 必 | 说明 |
shapes | Y | 构成此Geometry的一个或者多个THREE.Shape对象,可以传入数组 |
options | N |
应用到所有THREE.Shape的选项:
|
该类型是THREE.ShapeGeometry的最重要的部分,允许你创建自定义的形状。它提供以下方法和属性:
moveTo(x,y) 移动画笔到指定的位置 | ||
lineTo(x,y) 从当前位置向(x,y)绘制直线 | ||
quadraticCurveTo(aCPx, aCPy, x, y) 你可以使用两种方式绘制曲线:二次曲线、贝塞尔曲线。两种方式的不同之处在于如何指定曲线的曲率(curvature)。下图显示这两种曲线的差别: 除了曲线的两个端点以外,对于:
注意:起点都是画笔当前位置,不需要在参数中指定 |
||
splineThru(pts) 在一系列点之间绘制流线型(拟合)的曲线,参数必须是THREE.Vector2对象的数组 |
||
arc(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise) 绘制一个圆圈或者圆弧。(aX, aY)指定离开当前画笔位置的偏移量,aRadius表示半径,(aStartAngle, aEndAngle)表示起始、终止角度,aClockwise为布尔值,表示是否顺时针绘制 |
||
absArc(aX, aY, aRadius, aStartAngle, aEndAngle,AClockwise) 在绝对位置上绘制圆弧 |
||
ellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise) 绘制椭圆或者部分椭圆 |
||
absellipse(aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise) 在绝对位置上绘制椭圆或者部分椭圆 |
||
fromPoints(vectors) 根据THREE.Vector2或者THREE.Vector3数组绘制路径 |
||
holes THREE.Shape对象的数组,表示在当前形状上挖去的洞 |
||
makeGeometry(options) 基于此形状生成一个THREE.ShapeGeometry对象 |
||
createPointsGeometry(divisions) 把形状转换为一系列采样点的数组,divisions指定点的数量。你可以基于这些点生成一个线条对象:
|
||
createSpacedPointsGeometry(divisions) 与上面类似,但是生成一个Path对象 |
这是一个非常简单的三维图形,具有长宽高的盒子:
1 |
new THREE.BoxGeometry(10,10,10); |
可用属性:
属性 | 必 | 说明 |
width | Y | 宽度,沿着X轴 |
height | Y | 高度,沿着Y轴 |
depth | Y | 长度,沿着Z轴 |
widthSegments | N | 在三个方向上的分段数 |
heightSegments | N | |
depthSegments | N |
基于此类型,你可以绘制三维球体、不完整球体:
可以看到,你可以截取球体经度、纬度方向的任意片断。
代码示例:
1 |
new THREE.SphereGeometry(radius,widthSegments,heightSegments,phiStart,phiLength,thetaStart,thetaLength) |
该类型提供以下属性:
属性 | 必 | 说明 |
radius | N | 球体的半径,默认50 |
widthSegments | N | 垂直方向的分段数,默认8 |
heightSegments | N | 水平方向的分段数,默认8 |
phiStart | N | 在经度方向上,绘制球体的起点,向东绘制。范围0 ~ 2*PI |
phiLength | N | 在经度方向上,绘制的长度。范围0 ~ 2*PI |
thetaStart | N | 在纬度方向上,绘制球体的起点,向南绘制。范围0 ~ 2*PI |
thetaLength | N | 在纬度方向上,绘制的长度。范围0 ~ 2*PI |
可以绘制圆柱、圆筒、圆锥或者截锥。代码示例:
1 |
new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded) |
可用属性:
属性 | 必 | 说明 |
radiusTop | N | 上半径 |
radiusBottom | N | 下半截 |
height | N | 高度 |
radialSegments | N | 在上下底方向上的分段数,决定光滑度 |
heightSegments | N | 在高度方向上的分段数 |
openEnded | N | 是否上下底开放,默认false |
类似于甜甜圈的圆环面。代码示例:
1 |
new THREE.TorusGeometry(radius, tube, radialSegments,tubularSegments,arc) |
可用属性:
属性 | 必 | 说明 |
radius | N | 外半径 |
tube | N | 甜甜圈管道的半径 |
radialSegments | N | 分段数 |
tubularSegments | N | |
arc | N | 弧度,决定是不是绘制完整的甜甜圈,最大值2 * PI |
创建基于若干点的最小化凸面体。该形状不是Three.js核心库的组成部分。
允许你基于一个光滑曲线来创建形状。此曲线由一系列的点(Knots)指定,通常是拟合曲线。曲线围绕对象的中心Z轴转动,可以产生类似于花瓶、钟之类的形状。示例:
可用属性:
属性 | 必 | 说明 |
points | Y | 绘制曲线的基准点 |
segments | N | 分段数,数字越大则形状越光滑 |
phiStart | N | 开始弧度 |
phiLength | N | 绘制弧长 |
可以把2D图形凸起、抬高为3D图形。比如我们可以把上面章节中的ShapeGeometry抬高:
1 2 3 4 5 6 7 8 9 10 |
var options = { amount: 10, bevelThickness: 2, bevelSize: 1, bevelSegments: 3, bevelEnabled: true, curveSegments: 12, steps: 1 }; new THREE.ExtrudeGeometry( drawShape(), options ); |
运行效果图如下:
可用属性:
属性 | 必 | 说明 |
shapes | Y | 基于其进行凸起、抬高的THREE.Shape或者THREE.Shape数组 |
amount | N | 抬高的高度,默认100 |
bevelThickness | N |
在形状前面、后面,以及抬起的哪个侧面之间,创建一个斜坡 此厚度,即为圆滑斜坡给侧面“增加”的厚度的1/2 |
bevelSize | N | 斜坡的高度,此高度导致从前/后面看形状,其面积变大 |
bevelSegments | N | 斜坡分段数,让斜坡光滑 |
bevelEnabled | N | 是否启用斜坡,默认启用 |
curveSegments | N | 让曲线光滑 |
steps | N | 凸起生成的面的分段数,默认1 |
extrudePath | N | 沿着什么路径执行凸起,默认沿着Z轴,可以指定任意的路径 |
material | N | 用作前后面的材质的索引,如果希望前后面使用不同材质可以调用THREE.SceneUtils.createMultiMaterialObject() |
extrudeMaterial | N | 凸起面和斜坡使用的材质的索引 |
uvGenerator | N | UVGenerator,当为材质指定纹理时,指定UV Mapping——决定纹理的哪个部分给哪个面使用。默认THREE.ExtrudeGeometry.WorldUVGenerator |
与ExtrudeGeometry类似,这个类也是用于“凸起”的,只是它凸起的目标是3D的拟合曲线,而非2D图形。可用属性:
属性 | 必 | 说明 |
path | Y | 凸起的目标,一个THREE.SplineCurve3对象 |
segments | N | 分段数,路径越长,该值应该越大,默认64 |
radius | N | 管道的半径,默认1 |
radiusSegments | N | 管道截面分段数,默认8 |
closed | N | 是否闭合管道,默认false |
基于一个函数来生成几何图形。此函数有两个入参u、v,其返回值是一个三维向量,此向量作为几何图形的顶点,本质上是二维平面到三维空间的映射。
可用属性:
属性 | 必 | 说明 |
function | Y | 生成器函数,其返回值作为结果Geometry的顶点 |
slices | Y | u值被划分为多少子值,u的取值范围是0~1 |
stacks | Y | v值被划分为多少子值,v的取值范围是0~1 |
示例:
1 2 3 4 5 6 7 8 9 10 11 |
var radialWave = function ( u, v ) { var r = 50; var x = Math.sin( u ) * r; var z = Math.sin( v / 2 ) * 2 * r; var y = (Math.sin( u * 4 * Math.PI ) + Math.cos( v * 2 * Math.PI )) * 2.8; return new THREE.Vector3( x, y, z ); }; var mesh = createMesh( new THREE.ParametricGeometry( radialWave, 120, 120, false ) ); |
运行效果图:
该类型用于创建凸起、抬升的3D文本。可用属性:
属性 | 必 | 说明 |
size | N | 文本的尺寸,默认100 |
height | N | 凸起的高度 |
weight | N | 粗体设置,可选值bold、normal |
font | N | 字体名称,默认helvetiker |
style | N | 字体样式,可选值normal、italic |
bevelThickness | N | 斜坡设置,默认不启用斜坡 |
bevelSize | N | |
bevelSegments | N | |
bevelEnabled | N | |
curveSegments | N | 让曲线光滑 |
steps | N | 参考ExtrudeGeometry |
extrudePath | N | |
material | N | |
extrudeMaterial | N | |
uvGenerator | N |
你可以把Three.js的标准几何图形联合起来,形成复杂的新图形,这种技术叫做CSG(Constructive Solid Geometry,构造实体几何)。
为了支持CSG,我们需要使用到Three.js扩展ThreeBSP。该库提供了以下函数:
函数 | 说明 |
intersect | 基于两个既有Geometry的空间交叉部分(intersection)来生成新的Geometry |
union | 联合两个既有Geometry的空间,生成新的Geometry |
subtract | 从一个Geometry中挖去与另外一个Geometry重叠的部分,形成新的Geometry |
注意:这三个函数都基于Mesh的绝对位置执行计算。因此,如果你对组(或者应用多重材质)进行操作,可能得到意外的结果。
下面的代码示例了如何对两个球体进行二进制操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 创建BSP对象 var sphere1BSP = new ThreeBSP( sphere1 ); var sphere2BSP = new ThreeBSP( sphere2 ); var resultBSP; switch ( controls.actionSphere ) { case "subtract": resultBSP = sphere1BSP.subtract( sphere2BSP ); break; case "intersect": resultBSP = sphere1BSP.intersect( sphere2BSP ); break; case "union": resultBSP = sphere1BSP.union( sphere2BSP ); break; case "none": // noop; } // 转换为Mesh并添加到场景 result = resultBSP.toMesh(); result.geometry.computeFaceNormals(); result.geometry.computeVertexNormals(); scene.add(result); |
在前面的章节中,我们以及了解了Three.js的大部分重要组件:场景、镜头、灯光、图形、材质。本章主要研究一个重要的,但是迄今为止尚未提及的重要概念——粒子。
粒子(particles)某些时候也称为精灵(sprites),是场景中的小物体。这些物体很容易被大量的创建,以模拟雨、雪、烟以及其它多种有趣的特效。
需要注意,在较近版本的Three.js中,与粒子相关的物体的类型名称从THREE.ParticleSystem变为THREE.PointCloud。粒子本身的类型名称从THREE.Particle变为THREE.Sprite。
粒子是2D的平面,该平面总是正向面对镜头。下面的代码创建了100个粒子:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 粒子的材质 var material = new THREE.SpriteMaterial(); for ( var x = -5; x < 5; x++ ) { for ( var y = -5; y < 5; y++ ) { // 创建粒子 var sprite = new THREE.Sprite( material ); // 设置其位置 sprite.position.set( x * 10, y * 10, 0 ); // 添加到场景 scene.add( sprite ); } } |
当你不指定任何属性的时候,粒子被渲染为白色二维小方块。 所以,上面的代码会在场景中展示10 x 10的小方块阵列。
粒子接受的材质类型只有:THREE.SpriteCanvasMaterial、THREE.SpriteMaterial。
与Three.Mesh类似,THREE.Sprite也继承自THREE.Object3D。这意味着THREE.Mesh的很多属性/方法对于粒子也是可用的,你可以使用scale属性对其缩放、使用position让其移动。
虽然创建并移动粒子很简单,但是如果操控的粒子数量很大,你很快就会遇到性能问题。为此,Three.js提供了THREE.PointCloud用来统一处理大量的粒子。基于PointCloud的、与上面等效的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var geom = new THREE.Geometry(); // 点云材质 var material = new THREE.PointCloudMaterial( { size: 4, vertexColors: true, color: 0xffffff } ); for ( var x = -5; x < 5; x++ ) { for ( var y = -5; y < 5; y++ ) { // 每个粒子是三维空间中的一个点 var particle = new THREE.Vector3( x * 10, y * 10, 0 ); geom.vertices.push( particle ); geom.colors.push( new THREE.Color( Math.random() * 0x00ffff ) ); } } // 点的集合,点云 var cloud = new THREE.PointCloud( geom, material ); scene.add( cloud ); |
要创建点云,需要两个参数:
- 材质,使用颜色或者纹理来装饰粒子
- Geometry, 指定所有粒子的位置
下面再举一个例子:创建15000个随机亮度的绿色粒子构成的点云:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var geom = new THREE.Geometry(); var material = new THREE.PointCloudMaterial( { size: size, transparent: transparent, opacity: opacity, vertexColors: vertexColors, sizeAttenuation: sizeAttenuation, color: color } ); var range = 500; for ( var i = 0; i < 15000; i++ ) { // 位置随机的粒子 var particle = new THREE.Vector3( Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2 ); geom.vertices.push( particle ); var color = new THREE.Color( 0x00ff00 ); // 以色相、饱和度、亮度的方式设置颜色。随机亮度的绿色 color.setHSL( color.getHSL().h, color.getHSL().s, Math.random() * color.getHSL().l ); // 顶点颜色数组 geom.colors.push( color ); } |
当自动旋转点云时,你可以看到粒子满天飞舞的效果。
该材质的属性说明如下:
属性 | 说明 |
color | 点云(粒子系统)中所有粒子的颜色,如果vertexColors设置为true,并且设置了Geometry的colors属性,则该属性被覆盖 |
map | 指定该属性,你可以为粒子设置纹理。使用该属性,你可以让粒子看起来更像真实世界中的粒子,例如雪花 |
size | 粒子的尺寸,默认1 |
sizeAnnutation | 如果false,则所有粒子的大小一样。否则,其尺寸取决于粒子距离镜头的远近 |
vertexColors | 默认情况下,点云中所有粒子的颜色一致,设置该属性为THREE.VertexColors则Geometry的colors属性被用来指定粒子的颜色。默认值THREE.NoColors |
opacity | 与transparent联用,设置粒子的透明度 |
transparent | 默认false,如果设置为true,允许粒子具有透明度 |
blending | 渲染粒子时使用的混合模式 |
fog | 默认true,粒子是否被全局迷雾影响 |
你可以使用三种方式来基于HTML画布装饰(Style)粒子:
- 如果使用THREE.CanvasRenderer,你可以直接通过THREE.SpriteCanvasMaterial引用HTML5画布对象
- 如果使用THREE.WebGLRenderer,你需要一些额外的步骤来使用HTML5画布
在使用该渲染器时,你可以使用THREE.SpriteCanvasMaterial,直接把画布的输出作为粒子的纹理使用。SpriteCanvasMaterial这个材质是专门为CanvasRenderer准备的,支持以下属性:
属性 | 说明 |
color | 粒子的颜色,依据混合模式的设置,该颜色会和画布中图片进行混合 |
program | 一个函数,以画布上下文作为入参。在粒子渲染时该函数被调用,函数的输出被绘制为粒子 |
opacity | 粒子的透明度 |
transparent | 是否允许粒子透明 |
blending | 使用的混合模式 |
rotation | 用于旋转画布的内容 |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var canvasRenderer = new THREE.CanvasRenderer(); // ... // 抽取纹理的程序 var getTexture = function ( ctx ) { // the body ctx.translate( -81, -84 ); ctx.fillStyle = "orange"; ctx.beginPath(); // ... ctx.fill(); }; // 粒子材质 var material = new THREE.SpriteCanvasMaterial( { program: getTexture, color: 0xffffff } ); // 旋转 material.rotation = Math.PI; // 创建粒子 var range = 500; for ( var i = 0; i < 1500; i++ ) { var sprite = new THREE.Sprite( material ); sprite.position.set( /* random */ ); sprite.scale.set( 0.1, 0.1, 0.1 ); scene.add( sprite ); } |
使用该渲染器时, 你需要手工在内存中创建画布对象,完成2D图形绘制,并返回一个纹理对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
var getTexture = function () { var canvas = document.createElement( 'canvas' ); canvas.width = 32; canvas.height = 32; var ctx = canvas.getContext( '2d' ); // ... // 返回一个纹理对象 var texture = new THREE.Texture( canvas ); texture.needsUpdate = true; return texture; }; var geom = new THREE.Geometry(); var material = new THREE.PointCloudMaterial( { size: size, transparent: transparent, opacity: opacity, // 指定使用的THREE.Texture对象 map: getTexture(), sizeAttenuation: sizeAttenuation, color: color } ); var range = 500; for ( var i = 0; i < 5000; i++ ) { var particle = new THREE.Vector3( /* random */ ); geom.vertices.push( particle ); } cloud = new THREE.PointCloud( geom, material ); |
下面的代码演示了如何使用Canvas绘制一个径向渐变的光球:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var canvas = document.createElement('canvas'); canvas.width = 16; canvas.height = 16; var context = canvas.getContext('2d'); var gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 ); gradient.addColorStop(0, 'rgba(255,255,255,1)'); gradient.addColorStop(0.2, 'rgba(0,255,255,1)'); gradient.addColorStop(0.4, 'rgba(0,0,64,1)'); gradient.addColorStop(1, 'rgba(0,0,0,1)'); context.fillStyle = gradient; context.fillRect(0, 0, canvas.width, canvas.height); var texture = new THREE.Texture(canvas); texture.needsUpdate = true; return texture; |
可以使用此光球来装饰粒子,产生荧光那样的效果。
上一个粒子中,我们已经使用了纹理,纹理的图像从画布中抓取。
实际上,我们可以把任何图片作为纹理使用:
1 |
var texture = THREE.ImageUtils.loadTexture( "../assets/textures/particles/raindrop-3.png" ); |
注意:作为纹理的图片,大小必须是2的N次方,必须是正方形。
下面使用该纹理模拟下雨效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
var geom = new THREE.Geometry(); var material = new THREE.ParticleBasicMaterial( { size: size, transparent: transparent, opacity: opacity, map: texture, // 设置混合模式为相加,意味着雨滴图片中黑色背景部分(000000)不会被绘制——背景色 + 0 仍然是背景色 // 使用透明背景的纹理是不支持的 blending: THREE.AdditiveBlending, sizeAttenuation: sizeAttenuation, color: color } ); var range = 40; for ( var i = 0; i < 1500; i++ ) { var particle = new THREE.Vector3( Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2 ); // 设置随机的速度属性,备用 particle.velocityY = 0.1 + Math.random() / 5; particle.velocityX = (Math.random() - 0.5) / 3; geom.vertices.push( particle ); } cloud = new THREE.ParticleSystem( geom, material ); cloud.sortParticles = true; scene.add( cloud ); function render() { scene.children.forEach( function ( child ) { if ( child instanceof THREE.PointCloud ) { var vertices = child.geometry.vertices; // 在渲染循环中遍历处理所有粒子,根据速度设置其位置 vertices.forEach( function ( v ) { v.y = v.y - (v.velocityY); v.x = v.x - (v.velocityX); // 如果粒子超出显示范围,则重置其位置 if ( v.y <= 0 ) v.y = 60; if ( v.x <= -20 || v.x >= 20 ) v.velocityX = v.velocityX * -1; } ); } } ); requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
我们改进一下上面的例子,模拟更真实的下雪效果:
- 建立多个点云,来模拟不同大小的雪花
- 在Z轴方向改变雪花的位置,模拟三维空间中雪花的飘舞
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
function createPointCloud( name, texture, size, transparent, opacity, sizeAttenuation, color ) { var geom = new THREE.Geometry(); var color = new THREE.Color( color ); // 随机的改变亮度 color.setHSL( color.getHSL().h, color.getHSL().s, (Math.random()) * color.getHSL().l ); var material = new THREE.PointCloudMaterial( { size: size, transparent: transparent, opacity: opacity, map: texture, blending: THREE.AdditiveBlending, // 设置为false,表示对象不影响WebGL的depth buffer,这样不同的粒子系统就不会相互干扰 depthWrite: false, sizeAttenuation: sizeAttenuation, color: color } ); var range = 40; for ( var i = 0; i < 50; i++ ) { var particle = new THREE.Vector3( Math.random() * range - range / 2, Math.random() * range * 1.5, Math.random() * range - range / 2 ); // 雪花在三个轴的方向上都具有速度 particle.velocityY = 0.1 + Math.random() / 5; particle.velocityX = (Math.random() - 0.5) / 3; particle.velocityZ = (Math.random() - 0.5) / 3; geom.vertices.push( particle ); } var system = new THREE.PointCloud( geom, material ); system.name = name; system.sortParticles = true; return system; } // 创建多个粒子系统 function createPointClouds( size, transparent, opacity, sizeAttenuation, color ) { var texture1 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake1.png" ); var texture2 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake2.png" ); var texture3 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake3.png" ); var texture4 = THREE.ImageUtils.loadTexture( "../assets/textures/particles/snowflake5.png" ); scene.add( createPointCloud( "system1", texture1, size, transparent, opacity, sizeAttenuation, color ) ); scene.add( createPointCloud( "system2", texture2, size, transparent, opacity, sizeAttenuation, color ) ); scene.add( createPointCloud( "system3", texture3, size, transparent, opacity, sizeAttenuation, color ) ); scene.add( createPointCloud( "system4", texture4, size, transparent, opacity, sizeAttenuation, color ) ); } createPointClouds( controls.size, controls.transparent, controls.opacity, controls.sizeAttenuation, controls.color ); function render() { scene.children.forEach( function ( child ) { if ( child instanceof THREE.PointCloud ) { var vertices = child.geometry.vertices; vertices.forEach( function ( v ) { // 模拟三维飘落效果 v.y = v.y - (v.velocityY); v.x = v.x - (v.velocityX); v.z = v.z - (v.velocityZ); if ( v.y <= 0 ) v.y = 60; if ( v.x <= -20 || v.x >= 20 ) v.velocityX = v.velocityX * -1; if ( v.z <= -20 || v.z >= 20 ) v.velocityZ = v.velocityZ * -1; } ); } } ); requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
我们可以把多个Sprite放在一个图片中,然后通过偏移量来加载、使用。就像CSS Sprite那样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var spriteMaterial = new THREE.SpriteMaterial({ opacity: opacity, color: color, transparent: transparent, map: getTexture() // 这个纹理中包含五个横向排列的小图片,我们只需要其中一个 } ); // 纹理图片中,X轴(u)\Y轴(v)方向的偏移量 // 如果spriteNumber取2,表示需要第三个小图片,u-offset = 0.2 * 2 = 0.4,即u偏移为0.4 // 注意u、v的最大值都是1,表示整个图片的大小,因此每个图片的大小是0.2 spriteMaterial.map.offset = new THREE.Vector2(0.2 * spriteNumber, 0); // 如果不设置repeat,那么3、4、5几个小图都称为Sprite的一部分。而我们只需要第三个图片 // 1/5表示在u方向仅需要1/5长度,恰好是一个小图的大小 spriteMaterial.map.repeat = new THREE.Vector2(1 / 5, 1); spriteMaterial.depthTest = false; spriteMaterial.blending = THREE.AdditiveBlending; var sprite = new THREE.Sprite(spriteMaterial); |
点云基于你所提供的Geometry的顶点来渲染粒子。这意味着我们可以向它传递前面所学过的任何几何图形。
前面的章节我们了解到,可以通过ThreeBSP这个插件来创建复合的Mesh。本章我们将学习另外两种创建高级Geometry/Mesh的机制:
- 分组、合并:Three.js支持内置的分组/合并机制,允许基于现存的对象来创建Mesh/Geometry
- 加载模型:Three.js支持从多种外部格式来加载Mesh/Geometry
这个机制我们已经使用过,当为Geometry应用多个材质时,实际上Three.js就创建了组。
创建组非常容易,任何Mesh都可以包含子元素,你可以通过 add() 方法随时添加子元素:
1 2 3 4 5 6 7 8 9 10 |
sphere = createMesh(new THREE.SphereGeometry(5, 10, 10)); cube = createMesh(new THREE.BoxGeometry(6, 6, 6)); // 任何3D对象可以作为组的容器,Object3D是Mesh、Scene的超类 group = new THREE.Object3D(); // 最近版本的Three.js引入THREE.Group,专门用作组容器 // 向容器中添加其它Mesh group.add(sphere); group.add(cube); scene.add(group); |
当你针对组中的父对象进行移动、缩放、旋转等操作时,所有子对象将会被应用相同的操作。需要强调的是旋转操作,执行旋转的时候,是整个组围绕组的中心进行旋转,而不是每个元素绕着各自的中心旋转。
使用组时,你依然可以对单个元素进行移动、缩放、旋转等操作。但是需要注意,这些操作都是相对于父对象进行的。
大部分情况下,使用分组可以让你方便的操控大量的Mesh。但是性能问题也可能出现,因为使用分组时,每个对象依然需要被单独的处理、渲染。
使用 THREE.Geometry.merge() 你可以合并多个几何图形,然后创建单个Mesh:
1 2 3 4 5 6 7 8 |
var geometry = new THREE.Geometry(); for ( var i = 0; i < controls.numberOfObjects; i++ ) { var cubeMesh = createCube(); cubeMesh.updateMatrix(); // 提供被合并geometry的转换矩阵,确保geometry被正确的置位、旋转 geometry.merge( cubeMesh.geometry, cubeMesh.matrix ); } scene.add( new THREE.Mesh( geometry, cubeMaterial ) ); |
使用编程方式来模拟真实世界中复杂的形状是困难的,Three.js允许加载3D建模软件设计的Geometry/Mesh。
加载外部模型,是通过Three.js加载器(Loader)实现的。加载器把文本/二进制的模型文件转化为Three.js对象结构。
每个加载器理解某种特定的文件格式。
格式 | 说明 |
JSON |
Three.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene 利用该格式,你可以方便的重用复杂的Geometry或Scene |
OBJ / MTL |
OBJ是Wavefront开发的一种简单3D格式,此格式被广泛的支持,用于定义Geometry MTL用于配合OBJ,它指定OBJ使用的材质 Three.js提供了OBJExporter.js,使用它可以把Three.js模型导出为OBJ格式 |
Collada | 基于XML的格式,被大量3D应用程序、渲染引擎支持 |
STL |
STereoLithography的简写,在快速原型领域被广泛使用。3D打印模型通常使用该格式定义 Three.js提供了STLExporter.js,使用它可以把Three.js模型导出为STL格式 |
CTM | openCTM定义的格式,以紧凑的格式存储基于三角形的Mesh |
VTK | Visualization Toolkit定义的格式,用于声明顶点和面。此格式有二进制/ASCII两种变体,Three.js仅支持ASCII变体 |
AWD | 3D场景的二进制格式,主要被away3d引擎使用,Three.js不支持AWD压缩格式 |
Assimp | 开放资产导入库(Open asset import library)是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式 |
VRML |
虚拟现实建模语言(Virtual Reality Modeling Language)是一种基于文本的格式,现已经被X3D格式取代 尽管Three.js不直接支持X3D,但是后者很容易被转换为其它格式 |
Babylon |
游戏引擎Babylon的私有格式 |
PLY |
常用于存储来自3D扫描仪的信息 |
使用Three.js的JSON格式,你可以保存/加载一个Mesh或者整个场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
knot = createMesh(new THREE.TorusKnotGeometry()); scene.add(knot); // 保存 var result = knot.toJSON(); localStorage.setItem("json", JSON.stringify(result)); // 加载 var json = localStorage.getItem("json"); if (json) { var loadedGeometry = JSON.parse(json); var loader = new THREE.ObjectLoader(); // 将JSON解析为Mesh loadedMesh = loader.parse(loadedGeometry); loadedMesh.position.x -= 50; scene.add(loadedMesh); } |
首先引入必要的脚本:
1 2 |
<script type="text/javascript" src="../libs/SceneLoader.js"></script> <script type="text/javascript" src="../libs/SceneExporter.js"></script> |
代码示例:
1 2 3 4 5 6 7 8 9 10 11 |
var exporter = new THREE.SceneExporter(); var sceneJson = JSON.stringify(exporter.parse(scene)); localStorage.setItem('scene', sceneJson); var json = (localStorage.getItem('scene')); var sceneLoader = new THREE.SceneLoader(); // 最后一个参数 . 定义了相对URL,加载纹理时需要 sceneLoader.parse(JSON.parse(json), function(e) { scene = e.scene; }, '.'); |
市场上有大量3D建模软件,用来设计复杂的Mesh。开源领域比较流行的是Blender。
Three.js提供了针对Blender、Maya、3D Studio Max等流行软件的Exporter,可以把基于这些软件设计的模型直接导出为Three.js的JSON格式。
Exporter并非Three.js支持Blender的唯一途径,因为Three.js本身理解多种3D格式,而Blender也支持保存为这些格式。
- 复制/home/alex/JavaScript/three.js/utils/exporters/blender/addons目录到/home/alex/Applications/blender/2.78/scripts/addons
- 打开Blender,点击菜单栏File ⇨ User Preferences,选择Addons选项卡,搜索Three.js,勾选以启用:
- 点击File ⇨ Export,在弹出的菜单中应该可以看到Three.js项
点击File ⇨ Open,你可以打开既有模型文件并编辑。点击File ⇨ Export ⇨ Three.js(json),选择目标路径即可导出。
在导出对话框中,可以修改设置:
这样,导出的JSON会包含材质的声明,并且模型使用的纹理自动导出为图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var loader = new THREE.JSONLoader(); // 设置纹理的加载路径,JSON中仅仅包含纹理图片文件的名称,不包括目录前缀 loader.setTexturePath( './assets/' ); // 注意结尾的 / // 异步的加载操作,回调函数提供两个入参:加载的Geometry、加载到的材质的数组 loader.load( './assets/chair.json', function ( geometry, materials ) { // 回调参数是THREE.Geometry,THREE.Material[] var material = new THREE.MultiMaterial( materials ); var mesh = new THREE.Mesh( geometry, material ); // 放大以便看清 mesh.scale.x = 15; mesh.scale.y = 15; mesh.scale.z = 15; scene.add( mesh ); } ); |
此格式被Blender原生支持、Three.js也提供了相应的加载器。
首先引入必要的脚本:
1 2 3 4 |
<script type="text/javascript" src="../libs/OBJLoader.js"></script> <!-- 下面两个用于加载MTL --> <script type="text/javascript" src="../libs/MTLLoader.js"></script> <script type="text/javascript" src="../libs/OBJMTLLoader.js"></script> |
1 2 3 4 5 6 7 8 9 10 11 12 |
var loader = new THREE.OBJLoader(); loader.load( '../assets/models/pinecone.obj', function ( loadedMesh ) { // 回调参数是一个THREE.Object3D对象 var material = new THREE.MeshLambertMaterial( { color: 0x5C3A21 } ); loadedMesh.children.forEach( function ( child ) { child.material = material; child.geometry.computeFaceNormals(); child.geometry.computeVertexNormals(); } ); scene.add( loadedMesh ); } ); |
一个好的实践是,在回调中打印加载对象的结构。通常,加载的Geometry/Mesh表现为层次化的Group。理解此Group的结构,以便正确的应用材质,并执行额外的处理步骤。
此外,注意查看顶点的位置信息,然后估算是否需要进行缩放、如何放置镜头。
对Geometry调用computeFaceNormals、computeVertexNormals,以确保材质被正确的渲染。
如果你需要通过OBJ/MTL来加载模型,首先检查MTL的内容,确保它以相对路径来引用纹理图片。
下面的例子加载一个蝴蝶模型,需要注意,某些时候需要对材质进行微调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
var loader = new THREE.OBJMTLLoader(); loader.load( '../assets/models/butterfly.obj', '../assets/models/butterfly.mtl', function ( object ) { // 回调参数是一个THREE.Group对象 var wing2 = object.children[ 5 ].children[ 0 ]; var wing1 = object.children[ 4 ].children[ 0 ]; // 模型源文件中,蝴蝶翅膀的透明度设置有误导致看不见,这里手工调整一下材质 wing1.material.opacity = 0.6; wing1.material.transparent = true; // 禁用深度测试,避免渲染错误(不指定下面的代码来运行示例,可以看到翅膀中部分像素不停抖动) wing1.material.depthTest = false; // 默认情况下,Three.js仅仅会渲染一个面 wing1.material.side = THREE.DoubleSide; wing2.material.opacity = 0.6; wing2.material.depthTest = false; wing2.material.transparent = true; wing2.material.side = THREE.DoubleSide; object.scale.set( 140, 140, 140 ); mesh = object; scene.add( mesh ); object.rotation.x = 0.2; object.rotation.y = -1.3; } ); |
此格式的默认扩展名为.dae,也被广泛的使用。此格式用来定义场景、模型,甚至是动画。一个Collada模型同时包含了Geometry、材质的定义。
不意外的,要加载Collada格式同样需要引入Loader脚本:
1 |
<script type="text/javascript" src="../libs/ColladaLoader.js"></script> |
下面的代码,从Collada模型中导入一个卡车模型:
1 2 3 4 5 6 7 8 9 10 |
var loader = new THREE.ColladaLoader(); var mesh; loader.load( "../assets/models/dae/Truck_dae.dae", function ( result ) { // 从模型场景中克隆出一个对象 mesh = result.scene.children[ 0 ].children[ 0 ].clone(); mesh.scale.set( 4, 4, 4 ); // 添加到当前场景中 scene.add( mesh ); } ); |
需要注意的是,Collada加载器回调参数是如下结构:
1 2 3 4 5 6 7 |
var result = { scene: scene, // 场景对象,THREE.Scene,包括所有模型对象,都在其中 morphs: morphs, skins: skins, animations: animData, dae: {} }; |
本章仅关心scene属性中的对象。需要注意,纹理可能基于WebGL不支持的格式(例如.tga),你可能需要将其转换为.png格式,并编辑Collada文件。
我们之前的动画,都是基于渲染循环来实现——通知Three.js尽快的重新渲染。实现代码都是如下的模式:
1 2 3 4 5 6 7 8 9 |
render(); function render() { // 在此,可以修改模型属性 /* ... */ // 执行渲染 renderer.render( scene, camera ); // 调度下依次渲染 requestAnimationFrame( render ); } |
我们只需要手工触发一次render()调用,之后它就会被定期(通常是每秒60次)递归调用了。
基于这种方式,我们可以修改模型的各种属性——otation,scale, position, material, vertices, faces从而产生简单的动画效果。
尽管用鼠标选择对象和动画没有直接关系,但是为了深入理解镜头和动画,我们需要用到这一功能。
Three.js没有直接提供“点击”功能,但是我们可以基于THREE.Projector、THREE.Raycaster来判断鼠标当前对应到哪个物体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
document.addEventListener( 'mousedown', onDocumentMouseDown, false ); var projector = new THREE.Projector(); function onDocumentMouseDown( event ) { // 基于鼠标当前位置,创建一个3D向量 var x = ( event.clientX / window.innerWidth ) * 2 - 1; var y = -( event.clientY / window.innerHeight ) * 2 + 1; var z = 0.5; var vector = new THREE.Vector3( x, y, z ); // 把鼠标当前位置转换为Three.js场景中的坐标 —— 把2D屏幕坐标unproject为3D世界坐标 vector = vector.unproject( camera ); // 从相机所在位置发出一条射线,射到鼠标位置 var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); // 检查此射线穿过哪些物体 var intersects = raycaster.intersectObjects( [ sphere, cylinder, cube ] ); if ( intersects.length > 0 ) { console.log( intersects[ 0 ] ); intersects[ 0 ].object.material.transparent = true; intersects[ 0 ].object.material.opacity = 0.1; } } |
Tween.js是一个简单的JS库,可以基于给定的初值、终值自动计算所有中间值。这个中间值计算过程一般叫做tweening。示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var coords = { x: 0, y: 0 }; var tween = new TWEEN.Tween( coords ) .to( { x: 100, y: 100 }, 1000 ) // 在1秒内完成变换 .onUpdate( function () { // 每当值变化时,执行的回调 console.log( this.x, this.y ); } ) .start(); requestAnimationFrame( animate ); // requestAnimationFrame会自动把一个高精度的、从DOM加载到当前流逝的时间传递给回调 function animate( time ) { requestAnimationFrame( animate ); TWEEN.update( time ); } |
我们可以创建一个改变物体位置的循环动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var posSrc = { pos: 1 }; // 创建两个Tween并链接,形成循环动画 var tween = new TWEEN.Tween( posSrc ).to( { pos: 0 }, 5000 ); tween.easing( TWEEN.Easing.Sinusoidal.InOut ); var tweenBack = new TWEEN.Tween( posSrc ).to( { pos: 1 }, 5000 ); tweenBack.easing( TWEEN.Easing.Sinusoidal.InOut ); tween.chain( tweenBack ); tweenBack.chain( tween ); // 当值变化时,改变物体的顶点位置 var onUpdate = function () { var count = 0; var pos = this.pos; loadedGeometry.vertices.forEach( function ( e ) { var newY = ((e.y + 3.22544) * pos) - 3.22544; pointCloud.geometry.vertices[ count++ ].set( e.x, newY, e.z ); } ); pointCloud.sortParticles = true; }; tween.onUpdate( onUpdate ); tweenBack.onUpdate( onUpdate ); function render() { TWEEN.update(); // 以16.7次/秒的频率,更新Tween,然后重渲染场景 requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
跟踪球控制,允许你使用鼠标进行镜头的平移(鼠标左键)、缩放(鼠标中键)、旋转操作(鼠标右键)。
使用该控制方式,需要引入:
1 |
<script type="text/javascript" src="../libs/TrackballControls.js"></script> |
然后,创建控制器对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 关联到镜头 var trackballControls = new THREE.TrackballControls( camera ); // 设置速度 trackballControls.rotateSpeed = 1.0; trackballControls.zoomSpeed = 1.0; trackballControls.panSpeed = 1.0; function render() { // 获取上一次调用getDelta到现在流逝的时间 var delta = clock.getDelta(); // 传递此时间增量,控制器会根据移动速度来计算距离 trackballControls.update( delta ); requestAnimationFrame( render ); webGLRenderer.render( scene, camera ) } |
飞行控制,好像你在驾驶一架飞机,在场景中穿梭。
使用该控制方式,需要引入:
1 |
<script type="text/javascript" src="../libs/FlyControls.js"></script> |
示例代码:
1 2 3 4 5 6 7 |
var flyControls = new THREE.FlyControls(camera); flyControls.movementSpeed = 25; // 需要指向渲染场景的DOM元素 flyControls.domElement = document.querySelector('#WebGL'); flyControls.rollSpeed = Math.PI / 24; flyControls.autoForward = true; flyControls.dragToLook = false; |
第一人称视角。示例代码:
1 2 3 4 5 6 7 8 9 10 11 |
var camControls = new THREE.FirstPersonControls(camera); camControls.lookSpeed = 0.1; camControls.movementSpeed = 20; camControls.noFly = true; camControls.lookVertical = true; camControls.constrainVertical = true; camControls.verticalMin = 1.0; camControls.verticalMax = 2.0; // 场景最初渲染时,镜头的位置 camControls.lon = -150; camControls.lat = 120; |
与上一个类似,但是提供鼠标锁定功能。避免镜头一直移动导致晃眼。具体查看这个示例。
这种方式可以很方便的旋转、平移、缩放位于场景中心位置的物体。例如太空场景中的星球。示例代码:
1 2 3 4 5 6 |
var orbitControls = new THREE.OrbitControls(camera); orbitControls.autoRotate = true; var clock = new THREE.Clock(); ... var delta = clock.getDelta(); orbitControls.update(delta); |
当利用3D建模软件创建动画时,通常有两种机制—— 变形目标、骨骼动画。
使用变形目标(Morph targets),你可以定义模型的变形(deformed)版本——Mesh的一个关键位置(key position)。对于此变形版本,所有顶点的位置被记录下来。根据原始版本、变形版本的顶点位置的变化,可以方便的创建变化。其本质就是移动顶点的位置。
变形目标是定义动画最直接的方式,其缺点是对于大的Mesh和大的动画,模型文件会边的庞大。
Three.js支持手工的从一个关键位置移动到另一个,但是手工控制比较麻烦,你需要跟踪当前位置、需要变形到的目标位置。THREE.MorphAnimMesh把这些细节封装起来,我们通常直接使用该类。下面的代码示例了如何加载内置了变形目标的模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
var loader = new THREE.JSONLoader(); loader.load( '../assets/models/horse.js', function ( geometry, mat ) { var mat = new THREE.MeshLambertMaterial({ morphTargets: true, // 一定要设置材质的morphTargets为true,否则不支持动画 vertexColors: THREE.FaceColors } ); geometry.computeVertexNormals(); geometry.computeFaceNormals(); // 在创建MorphAnimMesh之前,要调用computeMorphNormals确保变形目标的所有法向量被正确的计算 // 此操作对于正确的灯光、阴影效果是必须的 geometry.computeMorphNormals(); if ( geometry.morphColors && geometry.morphColors.length ) { // 你可以为某个特定的变形目标的面定制颜色 var colorMap = geometry.morphColors[ 0 ]; for ( var i = 0; i < colorMap.colors.length; i++ ) { geometry.faces[ i ].color = colorMap.colors[ i ]; geometry.faces[ i ].color.offsetHSL( 0, 0.3, 0 ); } } meshAnim = new THREE.MorphAnimMesh( geometry, mat ); meshAnim.duration = 1000; meshAnim.position.x = 200; meshAnim.position.z = 0; scene.add( meshAnim ); }, '../assets/models' ); function render() { var delta = clock.getDelta(); webGLRenderer.clear(); // 推进动画 meshAnim.updateAnimation( delta * 1000 ); requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
Three.js的默认行为是一下子运行所有动画,如果为单个Geometry定义了多个动画,则可以通过 parseAnimations() 和 playAnimation(name,fps) 来运行其中一个动画。
这种方式允许你为模型定义骨骼,并且把顶点附着在骨骼上。当你移动骨骼的时候,所有相连的骨骼也跟随移动,并导致顶点移动,产生变形。
变形动画比较简单,Three.js只需要转换顶点位置就可以了。骨骼动画则要复杂一些,当你移动骨骼时,Three.js需要知道如何计算附着其上的皮肤(Mesh顶点)的位置。
下面这个例子是手工执行骨骼动画的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
var clock = new THREE.Clock(); var loader = new THREE.JSONLoader(); loader.load( '../assets/models/hand-1.js', function ( geometry, mat ) { // 注意设置skinning为true,否则不会看到任何骨骼移动的效果 var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } ); // 专门针对骨骼/皮肤几何图形的Mesh mesh = new THREE.SkinnedMesh( geometry, mat ); scene.add( mesh ); // 开始动画 tween.start(); }, '../assets/models' ); var onUpdate = function () { var pos = this.pos; // 转动手指 mesh.skeleton.bones[ 5 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 6 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 10 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 11 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 15 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 16 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 20 ].rotation.set( 0, 0, pos ); mesh.skeleton.bones[ 21 ].rotation.set( 0, 0, pos ); // 转动手腕 mesh.skeleton.bones[ 1 ].rotation.set( pos, 0, 0 ); }; var tween = new TWEEN.Tween( { pos: -1 } ) .to( { pos: 0 }, 3000 ) .easing( TWEEN.Easing.Cubic.InOut ) .yoyo( true ) // 下一次反向执行 .repeat( Infinity ) // 无限执行 .onUpdate( onUpdate ); render(); function render() { TWEEN.update(); requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
前面我们讨论过,Three.js支持多种外部模型格式。这些格式中的一部分,支持动画:
- 对于JSON格式,可以使用Blender with the JSON exporter导出
- Collada,该格式支持动画
基于Blender创建骨骼动画时,要注意以下几点:
- 模型的所有顶点,至少分配到一个顶点组(vertex group)中
- 顶点组的名称必须和控制它的骨骼的名称一致,这样Three.js才知道,移动骨骼时,需要修改哪些顶点
- 注意仅仅第一个Action被导出,因此要确保你需要导出的动画时第一个
- 创建关键帧时,最好选取所有骨头,即使某些不变化
- 导出模型时,需要保证模型处于rest pose,否则动画可能变形严重
- 导出时,注意勾选:Vertices、Faces、Normals、Skinning、UVs、Colors、Materials、Flip YZ、Skeletal animation
这样,骨骼的移动路径会被一同导出,在Three.js中可以简单的进行回放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var loader = new THREE.JSONLoader(); loader.load( '../assets/models/hand-2.js', function ( model, mat ) { var mat = new THREE.MeshLambertMaterial( { color: 0xF0C8C9, skinning: true } ); mesh = new THREE.SkinnedMesh( model, mat ); var animation = new THREE.Animation( mesh, model.animation ); mesh.rotation.x = 0.5 * Math.PI; mesh.rotation.z = 0.7 * Math.PI; scene.add( mesh ); // 此助手用于查看骨骼如何变化 helper = new THREE.SkeletonHelper( mesh ); helper.material.linewidth = 2; helper.visible = false; scene.add( helper ); // 开始播放动画 animation.play(); }, '../assets/models' ); render(); function render() { var delta = clock.getDelta(); if ( mesh ) { helper.update(); THREE.AnimationHandler.update( delta ); } requestAnimationFrame( render ); webGLRenderer.render( scene, camera ); } |
和从JSON格式导入动画的方式差不多,但是要注意Collada可以存储整个场景,包括镜头、灯光、动画,因此你需要找到需要使用的那个附带动画的Mesh:
1 2 3 4 |
var child = collada.skins[0]; // THREE.SkinnedMesh scene.add(child); var animation = new THREE.Animation(child, child.geometry.animation); animation.play(); |
纹理在Three.js中有多种不同的使用方式,你可以用纹理来定义Mesh的颜色,或者定义发光效果、凹凸(bump)、反射效果。
最基本的用例是加载纹理,并将其作为材质上的一个map。当你基于此材质创建Mesh时,Mesh被纹理着色:
1 2 3 4 5 |
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile) var mat = new THREE.MeshPhongMaterial(); mat.map = texture; var mesh = new THREE.Mesh(geom, mat); return mesh; |
作为纹理的图片,可以是PNG、GIF或者JPG格式,且大小必须是2的N次方。 需要注意,纹理图片的加载是异步的,如果你希望纹理加载完毕之后再进行渲染,可以:
1 |
texture = THREE.ImageUtils.loadTexture('texture.png', {},function() { renderer.render(scene); }); |
由于纹理图片的像素一般不能和面的像素一一对应,纹理需要放大或者缩小后使用。WebGL/Three.js提供了几个不同的选项。你可以设置纹理的magFilter、minFilter属性,来声明它将如何被缩放 。两个基本的取值为:
THREE.NearestFilter | 使用最临近的像素。当放大时,出现色块;当缩小时,丢失细节 |
THREE.LinearFilter | 基于周围四个像素的值,来决定一个正确的颜色。缩小时仍然会丢失细节,但是放大时会更加平滑,不会出现色块 |
除了这两个基本的取值之外,我们还可以使用mipmap——一系列纹理图片的集合,后者是前者的一半大小。mipmap可以在加载纹理时自动创建,结合以下filter取值使用:
THREE.NearestMipMapNearestFilter | 选取最匹配分辨率的mipmap,并应用NearestFilter规则。放大时仍然出现色块,但是看起来好很多 |
THREE.NearestMipMapLinearFilter | 选取最接近的两个mipmap级别,在两个级别上分别应用NearestFilter规则,得到中间结果。这两个中间结果随之传递给LinearFilter获得最终结果 |
THREE.LinearMipMapNearestFilter | |
THREE.LinearMipMapLinearFilter |
如果不明确指定,magFilter默认取值THREE.LinearFilter,minFilter默认取值THREE.LinearMipMapLinearFilter。
纹理本身是方形的,但是Three.js可以确保不管对于什么形状,材质都能正确的覆盖(wrap around),此保证由UV mapping实现。
所谓bump map,是一幅额外的纹理,用于在材质上添加更多的深度效果:
1 2 3 4 5 6 7 8 |
var texture = THREE.ImageUtils.loadTexture( "../assets/textures/general / " + imageFile) var mat = new THREE.MeshPhongMaterial(); mat.map = texture; var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump ) mat.bumpMap = bump; mat.bumpScale = 0.2; // 设置凸起的高度(负值则表示凹下的深度) var mesh = new THREE.Mesh( geom, mat ); return mesh; |
bump map通常都是灰度图,像素的密度代表了凸起的(相对)高度 。
对于normal map来说,高度信息没有被保存,但是法线的方向被保存了。使用normal map你可以在仅使用很少点、面的情况下创建具有复杂细节的模型:
1 2 3 4 5 6 7 8 |
var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile); var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal); var mat2 = new THREE.MeshPhongMaterial(); mat2.map = t; mat2.normalMap = m; mat.normalScale.set(1,1); // 设置凸起的高度(负值则表示凹下的深度) var mesh = new THREE.Mesh(geom, mat2); return mesh; |
normal map的缺点是不容易创建,需要使用Blender/Photoshop之类的特殊工具。
计算环境反射效果是非常消耗资源的操作。在Three.js中你可以模拟这种效果。步骤如下:
- 创建一个CubeMap对象,CubeMap是六个纹理的集合,可以被应用到Cube的六个面
- 使用CubeMap创建一个Box,此Box作为场景的环境,当你转动镜头时,看到的是此Box的内侧
- 把上述模拟环境的CubMap应用到需要反射效果的Mesh上面,Three.js可以确保其看起来就像是环境的反射
准备好纹理图片后,创建CubeMap非常容易。你需要的是能够组成完整环境的六幅图片:向前看时的图片(posz)、向后看时的图片(negz)、向上看的图片(posy)、向下看的图片(negy)、向右看的图片(posx)、向左看的图片(negx)。示例代码:
1 2 3 4 5 6 7 8 |
var path = "../assets/textures/cubemap/parliament/"; var format = '.jpg'; var urls = [ path + 'posx' + format, path + 'negx' + format, path + 'posy' + format, path + 'negy' + format, path + 'posz' + format, path + 'negz' + format ]; var textureCube = THREE.ImageUtils.loadTextureCube( urls ); |
如果你已经获得360度全景图片,可以利用工具将其切割为上面的六幅图。 或者直接让Three.js处理切割过程:
1 |
var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping()); |
Three.js提供了一个特殊的着色器,用来基于CubeMap来创建Skybox(环境):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var shader = THREE.ShaderLib[ "cube" ]; shader.uniforms[ "tCube" ].value = textureCube; var material = new THREE.ShaderMaterial( { fragmentShader: shader.fragmentShader, vertexShader: shader.vertexShader, uniforms: shader.uniforms, depthWrite: false, side: THREE.DoubleSide } ); var skybox = new THREE.Mesh( new THREE.BoxGeometry( 10000, 10000, 10000 ), material ); scene.add( skybox ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 此镜头看到的景象,将用于球体的动态反射效果 cubeCamera = new THREE.CubeCamera( 0.1, 20000, 256 ); scene.add( cubeCamera ); // 动态反射,不仅仅CubeMap出现在反射图像中,Mesh也是 // 两个动态反射的物体不支持相互反射 var dynamicEnvMaterial = new THREE.MeshBasicMaterial( { envMap: cubeCamera.renderTarget, side: THREE.DoubleSide } ); sphere = new THREE.Mesh( sphereGeometry, dynamicEnvMaterial ); scene.add( sphere ); // 静态反射 // 注意材质可以设置反射率 var envMaterial = new THREE.MeshBasicMaterial( { envMap: textureCube, side: THREE.DoubleSide, reflection: 1 } ); var cylinder = new THREE.Mesh( cylinderGeometry, envMaterial ); scene.add( cylinder ); function render() { // 此镜头看到的内容需要更新,否则动态反射物体漆黑一片 cubeCamera.updateCubeMap( renderer, scene ); requestAnimationFrame( render ); } |
材质的envMap属性可以设置为一个CubeMap对象,这样Mesh就可以反射CubeMap代表的环境。
Leave a Reply