一、前言
得空来学习下threejs,今天看看天空盒子该如何搭建。
网上找到的基本都是整张天空图,需要转换一下,不过转换的效果一般,果然还是得专门的素材来做效果才逼真。
附转换工具jaxry.github.io/panorama-to… 【需科学上网】
转换完直接点击图片下载,命名都给你整好了顺序。
有素材不用转换也行,但是图片的尺寸必须是正方形,最好是2的幂次方,不然会加载不出来,是全黑的状态。(一开始还让我怀疑是不是代码写错了)
六个面的顺序图 "px", "nx", "py", "ny", "pz", "nz"
二、前置条件
1、threejs版本
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.min.js"></script>
2、资源存放位置
public/assets/skybox/ 一共有6张图
3、灯光-环境光
其他光源可能照不到盒子,环境光最佳
const ambientLight = new THREE.AmbientLight(0xffffff) // 环境光
scene.add(ambientLight)
4、场景、相机、渲染器
这几样是必要的,就不赘述了。
我是直接拿了以前的threejs项目来做的,所以放源码有水字数的嫌疑,想看的哥们可以点这个链接vite2+vue3+ts+threejs搭建项目,摸鱼学习vue3和threejs的日子(二)
三、搭建方式
1、作为scene的background出现
这种方式最简便,也不会露馅,他是直接根据作为scene的背景出现,利用的是CubeTextureLoader这个api,只要还在这个场景,这个天空盒子就会一直存在。
友情提示,图片的尺寸必须为正方形尺寸,最好是2的幂次方,非正方形尺寸大概率会是全黑的结果。
代码如下
const urls = ["px", "nx", "py", "ny", "pz", "nz"].map(v => `/assets/skybox/${v}.png`)
scene.background = new THREE.CubeTextureLoader().load(urls)
还有另一种写法,setPath的写法,就是把路径里公共的部分提取出来,写法如下
const urls = ["px", "nx", "py", "ny", "pz", "nz"].map(v => `${v}.png`)
scene.background = new THREE.CubeTextureLoader().setPath(`/assets/skybox/`).load(urls)
效果如下,可以看到,不管相机视角如何移动,背景总归是能完整,不存在露馅的情况。
2、新建一个大盒子
可以新建一个大盒子,将整个场景的物品都存放在这个大盒子里,也可以充当天空盒子。
不过相机如果拉远了,是存在露馅的风险的,所以这种一般适合小场景的,比如房屋展览,车展一类的,限制好相机的移动范围,基本就不会露馅。
写法如下
var directions = ["px", "nx", "py", "ny", "pz", "nz"]//获取对象
var format = ".png"//格式
//创建盒子,并设置盒子的大小为( 200, 200, 200 )
var skyGeometry = new THREE.BoxGeometry( 200, 200, 200 )
//设置盒子材质
var materialArray = []
let texture
for (var i = 0; i < 6; i++) {
texture = new THREE.TextureLoader().load(path + directions[i] + format)//将图片纹理贴上
materialArray.push( new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
/*设置双面,因为你身处在盒子的内部,所以一定要设置双面或者镜像翻转。*/
}))
}
// var skyMaterial = new THREE.MeshFaceMaterial( materialArray ) // MeshFaceMaterial弃用了
var skyBox = new THREE.Mesh( skyGeometry, materialArray ) //创建一个完整的天空盒,填入几何模型和材质的参数
scene.add( skyBox )//在场景中加入天空盒
这里网上查到的都有MeshFaceMaterial,但我在写项目的过程中,却是发现MeshFaceMaterial好像已经被弃用了,因为mesh可以支持材质组了,可以用这个来给每个面单独设置纹理,不再需要MeshFaceMaterial额外生成一个材质组了。
效果如下,在近距离的话,跟第一种方式,效果差不多,但是一旦拉远,就是暴露这是一个大盒子。所以如果使用第二种的话,需要严格限制相机的移动。
3、着色器实现
着色器知识比较复杂,这里仅仅是理解使用。
着色器实现天空盒子的原理和第2种大盒子方法是一样的,区别在于,大盒子的6个面的纹理是由着色器材质来实现的。
话不多说,下面我边实现边讲一下里面的坑,而且代码具有时效性(threejs官网每次都悄咪咪改api名称,真的很苟啊)
首先同样是先加载纹理图片进来
// 尺寸必须为2的幂次方,或者是正方形尺寸的,不然都是全黑
// right、left、top、bottom、back、front
const urls = ["px", "nx", "py", "ny", "pz", "nz"].map(v => `/assets/skybox/${v}.png`)
let skyboxCubemap = new THREE.CubeTextureLoader().load(urls)
生成着色器材质
skyboxCubemap.format = THREE.RGBFormat
var cubeShader = THREE.ShaderLib['cube']; //传入纹理图片
cubeShader.uniforms['tCube'].value = skyboxCubemap // 错误写法
// cubeShader.uniforms['envMap'].value = skyboxCubemap // 当前的正确写法
// //着色器材质
var skyBoxMaterial = new THREE.ShaderMaterial( { //着色器材质
fragmentShader: cubeShader.fragmentShader, //定义自己的片元着色器
vertexShader: cubeShader.vertexShader, //定义自己的顶点着色器
uniforms: cubeShader.uniforms, //给着色器传入uniform变量的值
depthWrite: false, //决定这个材质是否影响WebGL的深度缓存
side: THREE.BackSide //侧面:反面
})
网上能找到的写法里,大多数都是写了tCube,但是会报错属性未定义的错误。
通过console打印,发现tCube已经更名为envMap,好一手釜底抽薪,淦。
更换新写法,并且添加到盒子,盒子添加到scene中去
const urls = ["px", "nx", "py", "ny", "pz", "nz"].map(v => `/assets/skybox/${v}.png`)
let skyboxCubemap = new THREE.CubeTextureLoader().load(urls)
skyboxCubemap.format = THREE.RGBFormat
var cubeShader = THREE.ShaderLib['cube']; //传入纹理图片
cubeShader.uniforms['envMap'].value = skyboxCubemap
//着色器材质
var skyBoxMaterial = new THREE.ShaderMaterial( { //着色器材质
fragmentShader: cubeShader.fragmentShader, //定义自己的片元着色器
vertexShader: cubeShader.vertexShader, //定义自己的顶点着色器
uniforms: cubeShader.uniforms, //给着色器传入uniform变量的值
depthWrite: false, //决定这个材质是否影响WebGL的深度缓存
side: THREE.BackSide //侧面:反面
})
// 占位中,下面加强定义获取的代码就写在这里
// 用着色器材质,生成一个200x200x200的正方体
const skyBox = new THREE.Mesh(
new THREE.BoxGeometry(200, 200, 200),
skyBoxMaterial
)
scene.add(skyBox)
我以为万事大吉了,结果运行结果给我当头来了一棒!
报错结果如下图
我根本不知道错误出在哪里,因为对着色器的知识还处于一知半解的状态,加上资料的匮乏(国内资料很少,而且大多数是同样的),在经历了漫长的寻找和尝试之后,我才在一篇文章里找到了解决方法,但我还是不够明白其中的原理,有明白的兄弟,教一下我吧。
解决方法如下,在生成200x200x200的正方体前,加强一下envMap的定义获取
Object.defineProperty( skyBoxMaterial, 'envMap', {
get: function () {
return this.uniforms.envMap.value;
}
})
效果就出来了,不过拉远还是会露馅。
四、小结
三种方式试下来,还是scene的方式最为简单,也不会露馅。
大盒子方式也简单,适合小场景。
着色器的话,感觉有点大材小用了,感兴趣的可以试试。
ps: 我是地霊殿_三無,一起学习进步吧。