three.js小白的学习之路。
最近在使用blender绘制一些厂房模型,不可避免的要开几个窗户,增加几个玻璃。
其实在几个月之前也遇到过类似的问题,就是在blender中将玻璃材质的折射、投射、金属度和粗糙度等参数调整好之后,在blender中看没有任何问题,但是导出glb,加载到three.js中渲染就会出现一些白色或者黑色的噪点,就类似于下面的这种情况:

在没有遮挡的时候是正常的玻璃材质,在后面有遮挡的时候就会出现黑色噪点。
TypeScript
{
const geo = new Three.PlaneGeometry(500, 500);
const mat = new Three.MeshBasicMaterial({
color: "gray",
});
const mesh = new Three.Mesh(geo, mat);
mesh.position.set(0, -100, 0);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);
}
{
const geometry = new Three.BoxGeometry(100, 100, 100);
const material = new Three.MeshPhysicalMaterial({
color: "white",
metalness: 0,
roughness: 0,
transmission: 0.9, // 透光率
ior: 1.8, // 折射
});
const mesh = new Three.Mesh(geometry, material);
mesh.position.set(0, 0, 0);
scene.add(mesh);
const gui = new GUI();
gui.add(material, "metalness", 0, 1, 0.01);
gui.add(material, "roughness", 0, 1, 0.01);
gui.add(material, "transmission", 0, 1, 0.01);
gui.add(material, "ior", 0, 2.333, 0.01);
}
可以通过GUI调试查看更改各个参数时,玻璃材质会体现出什么样的效果。
原因分析
在three.js的官网,关于ior折射以及transmission透光的解释是这样的:
ior: number,表示非金属材料的折射率,默认1.5,范围1.0-2.333。
transmission: number,表示投射程度,当其非0时,不透明度应该设置为1,默认0,范围0-1。
可以看出,上述两个属性是用来反射某个环境的,高度依赖于环境的反射和折射来模拟真实的效果。
blender中导出的模型是没有设置玻璃所要反射的环境的(blender中在渲染模式下,是有世界环境的),或者如上述的例子中,也是没有设置材质的环境的。这就导致玻璃材质不知道要反射什么东西,无法正确计算环境反射,导致渲染异常。
解决方法
既然需要反射的环境,就给他一个环境就可以了。这个参数是 envMap。
envmap: Texture,确保物理渲染正确的环境贴图,默认 null。
envMapIntensity: number,通过乘以对应的颜色值来计算最终的环境贴图,默认1。
我们加载一个环境贴图,并赋值给对应材质的envMap:
TypeScript
const textureCube = new Three.CubeTextureLoader().load([px, mx, py, my, pz, mz]);
// ..................
const material = new Three.MeshPhysicalMaterial({
color: "white",
metalness: 0,
roughness: 0,
transmission: 0.9, // 透光率
ior: 1.8, // 折射
envMap: textureCube,
});
效果如下:

可以看到,增加了envMap属性之后,就没有之前看到的黑色噪点了。问题解决。
这样会引入一个新的问题,我找不到贴切的纹理贴图怎么办?或者,本意只是说弄几个玻璃意思意思,拥有玻璃的特质,透过玻璃可以看到内部即可,不是真的要渲染周围的环境。
这就需要用到第二个参数,envMapIntensity,我们添加一个gui测一下:
TypeScript
const material = new Three.MeshPhysicalMaterial({
color: "white",
metalness: 0,
roughness: 0,
transmission: 0.9, // 透光率
ior: 1.8, // 折射
envMap: textureCube,
envMapIntensity: 1,
});
const gui = new GUI();
gui.add(material, "metalness", 0, 1, 0.01);
gui.add(material, "roughness", 0, 1, 0.01);
gui.add(material, "transmission", 0, 1, 0.01);
gui.add(material, "ior", 0, 2.333, 0.01);
gui.add(material, "envMapIntensity", 0, 1, 0.01);
效果如下:

可以看到,在envMapIntensity设置成0时,依然保留了玻璃的特性,但是不会再反应周围的环境了,有点类似于透明度opacity设置成0,但是又能清晰地看到有这么一块东西。
其他的实现玻璃效果的方法
除了修改物理材质这一个方法,还有其他的方法生成玻璃效果,也是three.js提供的API。
1.cubeCamera
cubeCamera用于实拍当前场景,并将渲染结果生成在一个离屏渲染目标上,用于提供对应的纹理。这种就可以应用到监控视频等场景中。
使用方法:
TypeScript
const cubeRenderTarget = new Three.WebGLCubeRenderTarget(512); // 参数是分辨率
const cubeCamera = new Three.CubeCamera(1, 1000, cubeRenderTarget);
const group = new Three.Group();
const geo1 = new Three.PlaneGeometry(1000, 1000);
const mat1 = new Three.MeshStandardMaterial({
metalness: 1,
roughness: 0,
envMap: cubeRenderTarget.texture,
});
const plane= new Three.Mesh(geo1, mat1);
group.add(plane);
const geo2 = new Three.SphereGeometry(100);
const mat2 = new Three.MeshStandardMaterial({ color: "blue" });
const ball= new Three.Mesh(geo2, mat2);
ball.position.set(0, 0, 500);
group.add(ball);
scene.add(group);
const render = () => {
requestAnimationFrame(render);
renderer.render(scene, camera);
cubeCamera.position.copy(plane.position);
cubeCamera.update(renderer, scene);
};
这里我们生成一个小球,和一个平面,并处理好相应位置。使用cubeCamera,并与plane的位置相同,这样cubeCamera拍摄出来的场景就是从平面的角度所能看到的场景目标。
注意,要在render里面循环更新cubeCamera,因为移动控制器的时候,不更新的话,就无法正常渲染。
2.Reflector
如果你运行了cubeCamera的例子,你会发现一切都没啥问题。于是你想:我再整一个镜子,让这两个镜子面对面,是不是就会出现现实世界中的那种"你中有我,我中有你"的效果。
如果你试了,你会发现,完全不行。
为什么?因为从一个镜子的方向看,他可以看到另一个镜子,但是另一个镜子就是一个平面而已,只是我们后加了材质和环境贴图罢了,反之亦然。
有没有更好的方法?有的,就是Reflector。
TypeScript
const group = new Three.Group();
const geo1 = new Three.PlaneGeometry(1000, 1000);
const mesh1 = new Reflector(geo1, {
color: "blue",
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
});
mesh1.name = "mirror1";
mesh1.position.z = -500;
group.add(mesh1);
const geo2 = new Three.PlaneGeometry(1000, 1000);
const mesh2 = new Reflector(geo2, {
color: "red",
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
});
mesh2.name = "mirror2";
mesh2.position.z = 500;
mesh2.rotateY(Math.PI);
group.add(mesh2);
scene.add(group);
对比之前的创建方式,可以发现,Reflector替代了PBR物理渲染材质,而是直接通过geometry生成模型。
这样就可以实现上述的效果。
只不过这个方法更耗性能,并且也不是无限的透射出另一面镜子,一般就两三回。