前言
本文我们将用three.js来模拟出一个反光球。效果如下:
前置知识
这是学习three.js系列
的第三篇,前两篇是:
关于three.js
基础知识,是放在了第一篇: 用three.js写一个下雨动画,可前往查看,后面的案例都不再重复。
几何体与材质
在基础知识篇中,我们了解了渲染器(Renderer)、场景(Scene)、照相机(Camera),坐标系,物体,光照等的基础使用,这里我们主要探讨物体相关。
在three.js
中,创建物体时,需要传入两个参数,一个是几何形状(Geometry),另一个是材质(Material)。
const object = new THREE.Mesh(geometry, material)
复制代码
几何体
几何体(Geometry)的功能是储存一个物体的顶点信息,这些顶点信息决定了物体的形状。在空间中绘制一个物体,如果使用WebGL
,需要程序员指定每个顶点的位置,而在three.js
中,你可以直接声明几何形状,比如立方体、平面、球体、圆柱体、四面体、八面体等,你只需要按照文档传入定义这些几何形状需要的参数即可。
一些例子:
const floorGeometry = new THREE.PlaneGeometry( 800, 1000 ) //创建一个平面几何体,传入宽高
const sphereGeometry = new THREE.SphereGeometry(350, 50, 50) //创建一个球体,传入半径和经度、纬度的分片
const doorGeometry = new THREE.BoxGeometry(100,210,40) //创建一个立方体,传入长、宽、高
复制代码
材质
材质就像物体的皮肤,决定了几何体的外表。例如,皮肤定义了一个几何体看起来是否像金属、透明与否,或者显示为线框。
材质(Material)的应用非常灵活,很多酷炫的3D效果都是因为材质。材质的类型有很多种,例如:
MeshBasicMaterial
:渲染后物体的颜色始终为该材质的颜色,不对光照产生反应,不会由于光照产生明暗、阴影效果。
const geometry = new THREE.BoxGeometry(); //不传入参数,则使用长宽高的默认值:1
const material = new THREE.MeshBasicMaterial({ //创建Basic材质
color: 0x00ff00
})
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
复制代码
MeshLambertMaterial
:只考虑漫反射,而不考虑镜面反射的效果,不适用于金属、玻璃等物体。如果物体使用MeshLambertMaterial
,则必须在场景中加入光照,否则物体不会显示。并且,物体最终的显示颜色由材质的color参数
和光照颜色共同决定。 下图是一些光照类型:下面是平行光(directionLight)和Lambert材质的结合,可以看到立方体每个面有了不同的明暗程度。
const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); //创建Lambert材质
const directionLight = new THREE.DirectionalLight(0xffffff) //创建平行光,参数是光的颜色
directionLight.position.set(10,10,10) //定义平行光方向
scene.add(directionLight) //将平行光添加到场景中
复制代码
MeshPhongMaterial
:考虑了镜面反射的效果,适合于金属、玻璃等。在同样的平行光环境下,将立方体的材质设为MeshPhongMaterial
,效果如下,可以看到,立方体会对光照产生镜面反射。
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
shininess: 100 //决定Phong材质的高光度,当shininess值为0时,表现和MeshLambertMaterial材质一样
});
复制代码
以上是一些比较基本的材质创建方式,更多的材质和材质参数参考官方文档。
纹理
之前,我们都是在创建的材质时传入color
值,这样创建的材质是单一颜色的。然而在更多时候,我们需要基于图像来生成材质。
下面我们创建一个六面都贴上图像的立方体。这里我们不考虑光照阴影等影响,使用MeshBasicMaterial
const geometry = new THREE.BoxGeometry();
const texture = new THREE.TextureLoader().load( `./images/pic1.jpg` ) //用TextureLoader加载图像文件
const material = new THREE.MeshBasicMaterial( {
map: texture // 将加载好的图像作为map传给材质
} )
const cube = new THREE.Mesh(geometry, material);
复制代码
注意,使用纹理加载器TextureLoader
来加载图像会有跨域限制,如果图像文件和当前html不在同域,且不允许跨域,加载图像文件就会失败。
所以以访问本地文件的方式(file://xxx)
打开html就不可行啦,可以用live-server
或者webpack-dev-server
搭建一个服务器。
live-server
搭建服务器的步骤:如何用live-server
搭建一个简易的服务器
本项目将使用webpack-dev-server
搭建服务器,有需要的童鞋可查看源码。
立方体每个面贴不同的图片
要为立方体每个面贴不同的图片,首先,准备6张图片。
使用纹理加载器
TextureLoader
分别加载6张图片,并设置到六个材质中:
const geometry = new THREE.BoxGeometry()
const materials = [] //材质数组
for( let i = 0; i < 6; i ++) {
//使用TextureLoader加载每一张图片
const texture = new THREE.TextureLoader().load( `../../images/reflection-sphere/${i+1}.jpg` );
//根据纹理生成材质,并加入材质数组中
materials.push( new THREE.MeshBasicMaterial({
map: texture
}));
}
//根据几何形状和材质数组生成物体
const cube = new THREE.Mesh(geometry, materials)
复制代码
效果如下:( 红色代表 X 轴,绿色代表 Y 轴,蓝色代表 Z 轴,使用 AxesHelper 生成 )
可以看出,材质数组中的材质会按照 X轴正方向、X轴负方向、Y轴正方向、Y轴负方向、Z轴正方向、Z轴负方向
的顺序,对立方体的面进行贴图。
纹理最基础的用法是作为贴图被添加在材质上(纹理映射),当你使用这样的材质创建物体(Mesh)时,物体的颜色来源于纹理。相较于纯颜色,基于纹理的材质可以更好地模拟现实世界。
实现 360 全景
回到我们这个小例子,我们的第一步是实现一个 360 全景。
boxGeometry方案
全景的实现原理:创造一个容器,通常是球体或正方体,在其内表面贴上图片,然后将相机放在容器的中心。
对比上面的例子,我们进行两步:
- 内表面贴图。我们只要需将
geometry
的scale
的一个属性设置为负值,材质就会应用在几何体的内表面。
const geometry = new THREE.BoxGeometry(10,10,10)
geometry.scale(-1, 1, 1) // 设置scale.x
// geometry.scale(1, -1, 1) 设置scale.y,会导致画面上下颠倒,所以通常都设置scale.x或者scale.z
// geometry.scale(1, 1, -1) 设置scale.z
const materials = []
for( let i = 0; i < 6; i ++) {
const texture = new THREE.TextureLoader().load( `../../images/reflection-sphere/${i+1}.jpg` );
materials.push( new THREE.MeshBasicMaterial({
map: texture}
));
}
const cube = new THREE.Mesh(geometry, materials)
cube.position.set(0, 0, 0)
复制代码
- 将相机放在容器的中心。
camera.position.set(0, 0, 0.01)
camera.lookAt(0,0,0)
复制代码
scene.background方案
另外一种实现全景的方案,就是使用THREE.CubeTextureLoader
加载6个图片,然后将加载好的图像纹理作为整个场景Scene
的背景,这样也能形成360度全景。
//准备6张图片,实现全景的图片也需要满足以下的顺序:
//X轴正方向、X轴负方向、Y轴正方向、Y轴负方向、Z轴正方向、Z轴负方向
const urls = [
'posx.jpg',
'negx.jpg',
'posy.jpg',
'negy.jpg',
'posz.jpg',
'negz.jpg'
]
//实例化CubeTextureLoader
const loader = new THREE.CubeTextureLoader()
//加载6个图像
const cubeMap = loader.setPath('../../images/reflection-sphere/').load(urls)
//将图像纹理作为场景的背景
scene.background = cubeMap 、
复制代码
可以看出,和第一种方案有着相同的效果。
两种方案的适用场景:第一种方案基于BoxGeometry,可以更好地控制在全景中特定坐标位置添加物体,适用于3d看房等室内场景,可以更好满足全景中物体点击、选中等需求。
第二种方案中,想要创建物体与全景中某一处进行坐标绑定比较困难,所以更适用于作为纯粹的环境全景。但只要不涉及全景中的坐标,这种方案对整个场景和相机的操作更加自由。在我们这个案例中,就将采用第二种方式实现全景。
实现反光球
添加球体
//创建球体形状
const sphereGeometry = new THREE.SphereGeometry(20, 30, 30)
//创建球体材质
const sphereMaterial = new THREE.MeshBasicMaterial({
color: 0xff00ff
})
//生成球体
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
//设置球体位置
sphere.position.set(0, 0, 0)
//将球体添加到场景中
scene.add(sphere)
复制代码
贴图实现反光
计算镜面反射效果对CPU的耗费是非常大的,而且通常会使用光线追踪算法。在Three.js
中你依然可以实现镜面反射效果,只不过是做一个假的。你可以通过创建一个物体所处环境的纹理来伪装镜面反射,并将它应用到指定的对象上。
envMap
在上面,我们分别用color
和map
创建了材质,这里我们将使用envMap(环境贴图).envMap
字面意思就是物体周边环境,比如你渲染一个有反光特点的物体,物体周边的环境肯定影响物体的渲染效果。
这里,我们就将全景纹理赋值给envMap,来模拟对四周环境的镜面反射。
之前实现360全景,使用了THREE.CubeTextureLoader
加载6张图片,Three.js会将这些图片整合到一起来创建一个无缝的纹理,上面全景是将这个纹理作为场景背景,而这里我们将这个纹理作为球体的环境贴图(envMap)。
//创建球体形状
const sphereGeometry = new THREE.SphereGeometry(20, 30, 30)
//将图像纹理作为球体的环境贴图(cubeMap为实现全景时用CubeTextureLoader加载的纹理)
const sphereMaterial = new THREE.MeshBasicMaterial({
envMap: cubeMap
})
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
sphere.position.set(0, 0, 0)
scene.add(sphere)
复制代码
这样的模拟只能反射指定的背景,如果场景中有其他物体,并不会出现在反光球中。
cubeCamera
要实时反射任何物体,可以借助cubeCamera
。
cubeCamera
会构造一个包含6个PerspectiveCameras
(透视摄像机)的立方摄像机, 并将其拍摄的场景渲染到一个WebGLCubeRenderTarget
上。
也就是说,我们可以使用cubeCamera
实时为场景中的物体拍摄照片,然后使用这些实时照片创建纹理。将这些纹理作为球体的环境贴图(envMap)就可以模拟实时的反射了。
//将要创建的纹理对象,定义目标纹理的一些参数
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 128, {
format: RGBFormat,
generateMipmaps: true,
minFilter: LinearMipmapLinearFilter
});
//创建cubeCamera
//1:近剪切面的距离;1000:远剪切面的距离;cubeRenderTarget:将要创建的纹理对象
const cubeCamera = new THREE.CubeCamera(1, 1000, cubeRenderTarget)
cubeCamera.position.set(0, 0, 0)
scene.add(cubeCamera)
复制代码
接下来,将生成的纹理作为球体的envMap:
const sphereMaterial = new THREE.MeshBasicMaterial({
envMap: cubeCamera.renderTarget //cubeCamera生成的纹理作为球体的环境贴图
})
const sphereGeometry = new THREE.SphereGeometry(20, 30, 30)
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
phere.position.set(0, 0, 0)
scene.add(sphere)
复制代码