全网最详Babylon.js入门教材(8)-深入理解天空盒

avatar
SugarTurbos Club 成员

Q:Babylon.js是什么?🤔️

Babylon.js 是一个强大的、开源的、基于 WebGLWebGPU3D引擎,用于在网页上创建和渲染 3D图形。它提供了一套丰富的 API和功能,包括物理引擎、粒子系统、骨骼动画、碰撞检测、光照和阴影等,可以帮助开发者快速创建复杂的 3D场景和交互。

Q:我为什么要写该系列的教材? 🤔️

因为公司业务的需要因而要在项目中使用到 Babylon.js,虽然官方的文档看起来覆盖面都挺全,且 playgroud 上的案例也都比较多,但一些具体的 API 或者功能属性也都没有特别多详细的介绍,包括很多使用方式的很多坑都得自己去源码中或者论坛上找。在将其琢磨完之后, 决定写一系列关于它的教材来帮助更多 babylon.js的使用者或者是期于学习 Web 3D的开发者。同时也是自己对其的一种巩固。

Babylon.js深入理解天空盒

在上一章节中,我们更多的是学习了如何创建 Scene以及它对应的生命周期,但这仅仅是第一步。在实际开发中,如何创建一个“好看”的 Scene或者如何让你的 Scene更真实(天空、宇宙、重力效果)才是我们着重要关注的内容。其中,天空盒这个概念是你一定得理解和掌握的,它在实际开发中应用的非常频繁。

阅读学习完了本章节后,你将掌握如何实现以下的效果。

房间的环境效果:

天空的环境效果:

Skyboxes 天空盒

首先还是先介绍一下天空盒是什么吧。说起这个,其实我们并不陌生,早在之前的章节中我们其实就已经接触过了,只不过大家可能没有发现而已。

例如在介绍弧度相机平移那一章节中,就有提到创建一个天空盒作为背景:

如果我们把相机拉到很远很远,飞出天空之外,去看看天空之外的世界会是什么呢?

我滴niang~,地球竟然是个方的,不是圆的,地球之外还是逃脱不了紫色的场景背景。

莫慌,这个方方正正的盒子就是 Skyboxes天空盒。在 3D中,我们如果想要去模拟一个这种天空的效果用常规方法好像还真不好模拟,总不可能真让天上飘那么多的“云朵模型”吧,那开发者在制作一个场景的时候,啥都不用干,光一个天空背景就耗了不少性能了。

所以聪明的人们就想到了,可以用天空盒这种方式将模拟环境添加到场景中,它是一个围绕场景的大型标准立方体,每个面上都有环境图片,与 3D 对象相比,渲染图像更容易、更快,而且对于远处的风景也同样适用。 展开的效果类似于下面这样:

为了更好的让开发者知道每张图是哪个位置,一般来说是这么表示的:

Babylon.js 官网上的图是这样的:

(它这里的图片可能画错了哈, x轴上的图片应该是 commonPart_px.jpgcomonPart_nx.jpg

CubeTexture 立方体纹理

上面提到了,天空盒既然是一个超大的立方体,那么我们在创建天空盒的时候,就要先创建一个超大立方体。wu~,例如先给个 size1000的立方体:

var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", { size:1000 }, scene);

然后关键的步骤就是创建一个 CubeTexture 的实例。CubeTexture它和我们前面学到的材质的纹理 (Texture类)一样,也是继承 BaseTexture类的,也属于纹理中的一种。虽然是纹理,但是它和正常纹理还不太一样,CubeTexture只能与 reflectionTexturerefractionTexture一起使用,而不能与其他材质属性(如 diffuseTexture)一起使用。

啥意思呢?我们正常创建一个材质,给这个材质设置纹理属性的时候,其实还是有很多纹理属性可以赋值的,例如自发光纹理 emissiveTexture,漫反射纹理 diffuseTexture

const standardMat = new BABYLON.StandardMaterial('standardMat', scene);

const texture = new BABYLON.Texture(
  'https://cdn.nlark.com/yuque/0/2024/png/451257/1721553497235-c398596c-37fc-4a7d-a3db-28da4c684601.png?x-oss-process=image%2Fformat%2Cwebp%2Fresize%2Cw_1024%2Climit_0',
  scene
);
// 赋值给 emissiveTexture
standardMat.emissiveTexture = texture
// 或者赋值给 diffuseTexture
standardMat.diffuseTexture = texture

但对于 CubeTexture,它只能赋值给 StandardMaterialreflectionTexture(反射纹理) 和 refractionTexture(折射纹理)。

我们等会再来介绍这两个怎么用,还是先继续看看 CubeTexture。首先和 Texture一样,它也是依赖图片的,但是它依赖的不是单张图片,而是一组图片(毕竟有六个面不是)。而且这些图片需要是 .jpg格式,要是正方形的,另外官网上提到了,为了提高效率,推荐使用 2 的幂大小,例如 1024 x 1024。

所以 CubeTexture的第一个参数需要的是一个存放六张图片的地址,例如放在一个名为 textures/skybox的文件夹中:

const cubTexture = new BABYLON.CubeTexture("textures/skybox", scene);

注意哈,文件夹中虽然放了六张图,但是 Babylon.js 并不知道你的哪张图片对应哪个面,所以它这里是用名字来进行约束的,也就是六张图的命名需要遵循一定规则:

  • skybox_nx.jpg(左)
  • skybox_ny.jpg(下)
  • skybox_nz.jpg(后)
  • skybox_px.jpg(右)
  • skybox_py.jpg(上)
  • skybox_pz.jpg(前)

OK, 我们按照要求创建一个 CubeTexture并赋值给 reflectionTexturereflectionTexture表示的是材质接收反射时要显示的纹理:

var createScene = function () {
    var scene = new BABYLON.Scene(engine);

    // 创建天空盒
    var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);
    // 创建标准材质,名为 skyBox
    var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
    // 创建一个 CubeTexture 并赋值给 skyBox 的 reflectionTexture 属性
    skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("textures/skybox", scene);
    // 别忘了把材质给到 skybox
    skybox.material = skyboxMaterial;

    var camera = new BABYLON.ArcRotateCamera("Camera", 0, 0, 10, new BABYLON.Vector3(0, 0, 0), scene);
    camera.attachControl();

    return scene;
};

(由于我的案例是在 Babylon.js 的 playground在线编辑预览工具上写的,这个工具内置了一些图片文件,所以我这边直接用 textures/skybox是有效果的)

如果你按上面这么写了,效果貌似不是我们想要的:

这也好理解,因为我们现在是在立方体盒子的内部,也就是立方体每个面的内侧,而运用之前所学的知识,材质默认是开启了背面剔除的,所以肯定看不到里面的面咯。

backFaceCulling 背面剔除

因此还需要将材质的背面剔除给关掉:

var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);
var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("textures/skybox", scene);
// 关闭背面剔除,让我们能看到立方体的内侧
skyboxMaterial.backFaceCulling = false;

效果如下:

好,现在天空好像是有了,但效果咋感觉不太好呢?如果把相机角度调到天空的顶部,明显能看到有立方体的“棱”。确实也是合理的,因为天空盒本身就没指定太多图片,三条边相交的地方也就只有三张图,确实会显得有些突兀不协调。

reflectionTexture.coordinatesMode

诶~ Babylon.js 也给你想好了该怎么办,你可以指定 reflectionTexture.coordinatesMode属性为 BABYLON.Texture.SKYBOX_MODE,告诉它这个纹理以天空盒的方式进行渲染,它内部会去帮你把图与图之间的突兀给处理好:

var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);
var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
skyboxMaterial.backFaceCulling = false;
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("textures/skybox", scene);
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;

效果如下:

disableLighting 禁用灯光

好,一直到这一步的话,我们的天空其实已经差不多了,但如果这时候你向场景添加一个 sphere的话,会发现它是没有光照的:

var sphere = BABYLON.MeshBuilder.CreateSphere('sphere', { segments: 30, diameter: 1 }, scene);
sphere.position = new BABYLON.Vector3(0, 0, 0);

效果:

因为我们此时并没有在场景中添加任何的光源,天空上的“太阳”只是贴图里的一部分而已,并不是一个真的光源。

那回到实际情况,我们的场景基本都会有光源,我们就来创建一个:

var light = new BABYLON.HemisphericLight('default light', BABYLON.Vector3.Up(), scene);

效果如下:

好像“太阳光是不是太亮了”,也就是感觉太亮了。我们一般来说可以通过材质上的 disableLighting属性直接把所有光对天空盒材质的反射给禁用掉:

skyboxMaterial.disableLighting = true;

或者使用另一种方式:因为 StandardMaterial的漫反射颜色 diffuseColor和高光颜色 specularColor都是白色,所以会显得很亮,我们还可以直接把它们三个颜色通道都设置为0,让它不那么亮:

skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);

效果如下:

至此,我们就可以创建出一个模拟天空环境的场景了。

完整代码如下:

var scene = new BABYLON.Scene(engine);

// 创建天空盒
var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);
// 创建标准材质,名为 skyBox
var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
// 创建一个 CubeTexture 并赋值给 skyBox 的 reflectionTexture 属性
skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("textures/skybox", scene);
// 关闭背面剔除,让我们能看到立方体的内侧
skyboxMaterial.backFaceCulling = false;
skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;

// 关闭灯光的影响
skyboxMaterial.disableLighting = true;
// skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
// skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);

// 别忘了把材质给到 skybox
skybox.material = skyboxMaterial;

var camera = new BABYLON.ArcRotateCamera("Camera", 2.6, 2, 50, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl();

var light = new BABYLON.HemisphericLight('default light', BABYLON.Vector3.Up(), scene);

var sphere = BABYLON.MeshBuilder.CreateSphere('sphere', { segments: 30, diameter: 1 }, scene);
sphere.position = new BABYLON.Vector3(0, 0, 0);

在线预览地址:

playground.babylonjs.com/#UU7RQ#4173

CubeTexture 的其他参数

CubeTexture的构造函数:

/**
  创建一个立方体纹理,例如用于反射。它可以基于dds或六个图像,以及预过滤的数据。
  @param rootUrl 定义纹理的url或六个图像的根名称
  @param sceneOrEngine 定义纹理所附加的场景或引擎
  @param extensionsOrOptions 定义在使用六个图像的情况下添加到图片名称的后缀,如_px.jpg,或创建立方体纹理的所有选项集
  @param noMipmap 定义是否应创建mipmap
  @param files 定义要按照以下顺序加载的六个文件的不同面:px, py, pz, nx, ny, nz
  @param onLoad 定义在文件加载结束时如果没有错误发生触发的回调
  @param onError 定义在加载过程中发生错误时触发的回调
  @param format 定义一旦加载纹理后要使用的内部格式
  @param prefiltered 定义纹理是否从预过滤的数据创建
  @param forcedExtension 定义要使用的扩展名(强制加载特定类型的文件),以防它与文件名不同
  @param createPolynomials 定义是否需要从纹理数据创建多项式谐波
  @param lodScale 定义应用于环境纹理的比例。这管理了根据粗糙度用于IBL的LOD级别的范围
  @param lodOffset 定义应用于环境纹理的偏移。这管理了根据粗糙度用于IBL的第一个LOD级别
  @param loaderOptions 要传递给加载器的选项
  @param useSRGBBuffer 定义纹理是否必须在sRGB GPU缓冲区中加载(如果GPU支持)(默认:false)
  @returns 立方体纹理
*/
constructor(
    rootUrl: string,
    sceneOrEngine: Scene | AbstractEngine,
    extensionsOrOptions: Nullable<string[] | ICubeTextureCreationOptions> = null,
    noMipmap: boolean = false,
    files: Nullable<string[]> = null,
    onLoad: Nullable<() => void> = null,
    onError: Nullable<(message?: string, exception?: any) => void> = null,
    format: number = Constants.TEXTUREFORMAT_RGBA,
    prefiltered = false,
    forcedExtension: any = null,
    createPolynomials: boolean = false,
    lodScale: number = defaultLodScale,
    lodOffset: number = 0,
    loaderOptions?: any,
    useSRGBBuffer?: boolean
) {}

可以看到,CubeTexture构造函数的参数很多哈,有几个参数我觉得是可以着重先关注的:

extensionsOrOptions

第三个参数 extensionsOrOptions:默认为 null,如果你传入 string[] 的话,就表示【在使用六个图像的情况下添加到图片名称的后缀】。啥意思呢?原本 Babylon.js 要求你六张图都得按 _nx.jpg_px.jpg这种图片名称后缀命名,但这个参数可以让你指定其他命名规则。如果你传入的是一个对象的话,这个对象就表示后面那一大长串的可选项参数了,例如 noMipmapfiles这种。

ICubeTextureCreationOptions在源码中的定义如下:

export interface ICubeTextureCreationOptions {
    /** Defines the suffixes add to the picture name in case six images are in use like _px.jpg */
    extensions?: string[];

    /** noMipmap defines if mipmaps should be created or not */
    noMipmap?: boolean;

    /** files defines the six files to load for the different faces in that order: px, py, pz, nx, ny, nz */
    files?: string[];

    /** buffer to load instead of loading the data from the url */
    buffer?: ArrayBufferView;

    /** onLoad defines a callback triggered at the end of the file load if no errors occurred */
    onLoad?: () => void;

    /** onError defines a callback triggered in case of error during load */
    onError?: (message?: string, exception?: any) => void;

    /** format defines the internal format to use for the texture once loaded */
    format?: number;

    /** prefiltered defines whether or not the texture is created from prefiltered data */
    prefiltered?: boolean;

    /** forcedExtension defines the extensions to use (force a special type of file to load) in case it is different from the file name */
    forcedExtension?: any;

    /** createPolynomials defines whether or not to create polynomial harmonics from the texture data if necessary */
    createPolynomials?: boolean;

    /** lodScale defines the scale applied to environment texture. This manages the range of LOD level used for IBL according to the roughness */
    lodScale?: number;

    /** lodOffset defines the offset applied to environment texture. This manages first LOD level used for IBL according to the roughness */
    lodOffset?: number;

    /** loaderOptions options to be passed to the loader */
    loaderOptions?: any;

    /** useSRGBBuffer Defines if the texture must be loaded in a sRGB GPU buffer (if supported by the GPU) (default: false) */
    useSRGBBuffer?: boolean;
}

files:

上面我们提到了的案例都是基于 playground上去编写的,所以通过 textures/skybox这个路径去拿到它自带的天空盒图片。你可以点击 playground工具栏上的下载按钮把这段代码下载下来,同时也会下载到这几张贴图资源:

(有一些小伙伴可能会因为网络的原因下载下来的 textures里是空的)

但这种直接传递文件夹目录地址,并要求里面图片文件名按要求命名的规则,你说要是在一些桌面端开发的项目中(例如前端可以用 electron开发桌面端应用)也许还可以,但在一些 Web应用上,可能会运用到远程地址,确实是不太适合。

这时候我们可以通过第五个参数直接指定六张图片的地址:

const cubeTexture = new BABYLON.CubeTexture(
  "textures/skybox",
  scene,
  null,
  false,
  ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg']
);

妈耶~这好像才是天空盒的正确打开方式是吧 [哭笑~] (莫慌,实际上下面还会介绍到 CubeTexture.CreateFromImages(),可能更适合你)。

onLoad() 和 onError() :

再就是两个回调函数,加载成功了和加载失败了,也许也是你需要的。

CubeTexture.CreateFromImages

这时候咱还可以使用 CubeTexture.CreateFromImages来创建天空盒贴图。它有三个参数,分别是:

  • files: string[]:天空盒图片的路径地址
  • scene: Scene:场景
  • noMipmap?: boolean:可选参数,指定是否不使用 mip映射,默认值为 false

用法上:

const cubeTexture = new BABYLON.CubeTexture.CreateFromImages(
  ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg'],
  scene,
);

使用 CubeTexture.CreateFromImages创建天空盒贴图的话,只需要传递两个参数就行了,应该是最方便的一种用法了。

不过不得不提一嘴,CubeTexture.CreateFromImages内部的实现也不过是对 CubeTexture实例化做一层封装而已:

public static CreateFromImages(files: string[], scene: Scene, noMipmap?: boolean): CubeTexture {
    let rootUrlKey = "";

    files.forEach((url) => (rootUrlKey += url));

    return new CubeTexture(rootUrlKey, scene, null, noMipmap, files);
}

所以本质上这几种使用方式都没什么区别。

infiniteDistance 天空盒跟随相机

一直到上一步,天空盒看起来已经创建的差不多了,诶~别急,接下来还有一个重要的概念要介绍。

在开头介绍天空盒的时候我们就已经提到,天空盒本质是一个很大的 box,相机在这个 box内,看到 box的内壁,来模拟天空环境。那么正常情况下没对相机做任何限制的时候,相机是可以脱离出 box的:

相机与天空盒的关系:

在实际情况中,这肯定不是我们所期望的,我们期望用户根本不知道天空盒这个东西,所处的环境就一直在天空盒中。

那聪明的你肯定想到了,我们可以利用之前所学的知识,对相机的 radius做限制,例如这里我们的天空盒的 size1000的话,那么相机的 upperRadiusLimit应该要设置为 size的一半才能保证不出现问题:

// 创建天空盒
var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);

// 省略天空盒的设置代码

// 创建相机
var camera = new BABYLON.ArcRotateCamera("Camera", 2.6, 2, 50, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl();
// 限定最大 radius
camera.upperRadiusLimit = 550;

设置为 size的一半也好理解,因为我们这个是弧度相机,相机的 target为原点(0,0,0),与天空盒这个 box的原点是重合的,那么相机的半径肯定是要限制在 box内部才不会出现问题。

如下:

如果我们把 upperRadiusLimit再多设大一点的话,在某些角度就会出现这个问题:

可以看到相机会碰到天空盒的内壁,导致相机镜头出现被遮挡的效果,所以这个 upperRadiusLimit的设置也是有讲究的哦。

OK,看起来使用 cameraupperRadiusLimit是可以解决大部分情况的,但有一种情况你可能会遇到。

假设我们的天空盒还是 1000,不对相机的 radius做任何限制,然后有一个 sphere的坐标是 (550, 0, 0),超出了天空盒的范围到了天空盒之外。

那这时候如果相机在天空盒内部的话你肯定怎样都看不到这个 sphere了,因为它已经被天空盒遮挡住了。

此时我们把相机拉出天空盒之外,确实也能看到这个球:

小球、天空盒、相机的位置关系如下:

在线查看案例和代码:playground.babylonjs.com/#UU7RQ#4175

如果一定要看到这个小球的话,我们可以通过调整相机的 target,让它的目标定位到小球上。

// 相机的 target 为 (550, 0, 0)
var camera = new BABYLON.ArcRotateCamera("Camera", 2.6, 2, 50, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl();
camera.target = new BABYLON.Vector3(550, 0, 0);
camera.radius = 200;

// 球的半径为 50, 位置为 (550, 0, 0)
var sphere = BABYLON.MeshBuilder.CreateSphere('sphere', { segments: 30, diameter: 50 }, scene);
sphere.position = new BABYLON.Vector3(550, 0, 0);

效果:

此时转动相机视角,可以看到小球了,但是小球还是在天空盒之外,这显然不是我们所期望的。

我们所期望的是物体应该始终处于我们设置的“环境”当中,你当然可以通过设置物体的位置始终在天空盒内来完成,但有些时候物体的位置可能就是超出了,这时候该怎么办呢?

诶,我们换个角度来想问题。我们之所以能看到场景里的这些东西,都是依靠相机这个“眼睛”,如果我们能让天空盒始终跟着相机动,那我们不管看向哪里,都始终所处在天空盒之内了呢?

介绍了这么久的前置知识,终于引出这一部分的主角了,其实要解决这个问题非常的简单,只需要设置 skybox.infiniteDistance = true; 即可 [哭笑~]。

// 创建天空盒
var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", {size:1000.0}, scene);
// 设置天空盒跟随相机
skybox.infiniteDistance = true;

// 省略天空盒其它的设置代码

// 相机的 target 为 (550, 0, 0)
var camera = new BABYLON.ArcRotateCamera("Camera", 2.6, 2, 50, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl();
camera.target = new BABYLON.Vector3(550, 0, 0);
camera.radius = 200;

// 球的半径为 50, 位置为 (550, 0, 0)
var sphere = BABYLON.MeshBuilder.CreateSphere('sphere', { segments: 30, diameter: 50 }, scene);
sphere.position = new BABYLON.Vector3(550, 0, 0);

解决方案虽然很简单,但我认为主要的还是要弄清楚这个原理,这样才能帮助我们应对实际开发中可能会遇到的各种奇奇怪怪的问题。

效果如下:

案例在线查看:playground.babylonjs.com/#UU7RQ#4177

其它天空盒的素材

官网提供的天空盒素材还是有限的,而且大多时候你可能需要定义自己的天空盒素材,这时候可以通过一些网站将普通的一张图片,转成6张天空盒的图片。

例如你可以在:jaxry.github.io/panorama-to… 上面进行转换:

或者在网上搜索“天空盒素材”也可以找到很多。

这里有一些开源的天空盒素材供大家使用:

gitcode.com/open-source…

(来源:blog.csdn.net/baidu_29701…

后语

知识无价,支持原创!这篇文章主要向大家介绍了什么是天空盒,并且教大家如何在 Babylon.js 中使用天空盒。当然,仅仅只有天空盒,我们的 3D 场景距离真实世界还是有不少的差距,那在下一章节我们将继续教大家如何丰富我们的场景。

喜欢霖呆呆的小伙伴还希望可以关注霖呆呆的公众号 LinDaiDai

我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉。

你的鼓励就是我持续创作的主要动力 😊。

其它相关文章推荐: