threejs天空盒子搭建

1,734 阅读4分钟

一、前言

得空来学习下threejs,今天看看天空盒子该如何搭建。

网上找到的基本都是整张天空图,需要转换一下,不过转换的效果一般,果然还是得专门的素材来做效果才逼真。

附转换工具jaxry.github.io/panorama-to… 【需科学上网

转换完直接点击图片下载,命名都给你整好了顺序。

有素材不用转换也行,但是图片的尺寸必须是正方形,最好是2的幂次方,不然会加载不出来,是全黑的状态。(一开始还让我怀疑是不是代码写错了)

六个面的顺序图 "px", "nx", "py", "ny", "pz", "nz"

image.png

二、前置条件

1、threejs版本

<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.min.js"></script>

2、资源存放位置

public/assets/skybox/ 一共有6张图

image.png

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)

效果如下,可以看到,不管相机视角如何移动,背景总归是能完整,不存在露馅的情况。

天空盒子1.gif

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额外生成一个材质组了。

image.png

效果如下,在近距离的话,跟第一种方式,效果差不多,但是一旦拉远,就是暴露这是一个大盒子。所以如果使用第二种的话,需要严格限制相机的移动。

天空盒子2.gif

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,但是会报错属性未定义的错误。

image.png

通过console打印,发现tCube已经更名为envMap,好一手釜底抽薪,淦。

image.png

更换新写法,并且添加到盒子,盒子添加到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) 

我以为万事大吉了,结果运行结果给我当头来了一棒!

报错结果如下图 image.png

我根本不知道错误出在哪里,因为对着色器的知识还处于一知半解的状态,加上资料的匮乏(国内资料很少,而且大多数是同样的),在经历了漫长的寻找和尝试之后,我才在一篇文章里找到了解决方法,但我还是不够明白其中的原理,有明白的兄弟,教一下我吧。

解决方法如下,在生成200x200x200的正方体前,加强一下envMap的定义获取

 Object.defineProperty( skyBoxMaterial, 'envMap', {
    get: function () {
        return this.uniforms.envMap.value;
    }
  })

效果就出来了,不过拉远还是会露馅。

天空盒子3.gif

四、小结

三种方式试下来,还是scene的方式最为简单,也不会露馅。

大盒子方式也简单,适合小场景。

着色器的话,感觉有点大材小用了,感兴趣的可以试试。

ps: 我是地霊殿_三無,一起学习进步吧。

Snipaste_2022-07-19_15-30-26.jpg