用three.js写一个反光球:360全景 + 镜面反射

8,756 阅读7分钟

前言

本文我们将用three.js来模拟出一个反光球。效果如下:

reflection.gif

前置知识

这是学习three.js系列的第三篇,前两篇是:

用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参数和光照颜色共同决定。 下图是一些光照类型: image.png 下面是平行光(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张图片。 image.png 使用纹理加载器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方案

全景的实现原理:创造一个容器,通常是球体或正方体,在其内表面贴上图片,然后将相机放在容器的中心。

对比上面的例子,我们进行两步:

  1. 内表面贴图。我们只要需将geometryscale的一个属性设置为负值,材质就会应用在几何体的内表面。
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)
  1. 将相机放在容器的中心。
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)

image.png

贴图实现反光

计算镜面反射效果对CPU的耗费是非常大的,而且通常会使用光线追踪算法。在Three.js中你依然可以实现镜面反射效果,只不过是做一个假的。你可以通过创建一个物体所处环境的纹理来伪装镜面反射,并将它应用到指定的对象上。

envMap

在上面,我们分别用colormap创建了材质,这里我们将使用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)
图片替换文本