threeJs学习笔记(六)

模型导入

目前比较火的模型文件格式是GLTF格式,它可以存储更多的数据,比如一整个场景的对象、对象嵌套、骨骼、动画等

一个GLTF模型的仓库:
https://github.com/KhronosGroup/glTF-Sample-Assets

模型的格式

gltf常常存在4种格式:

  • gltf
  • gltf-binary
  • gltf-draco
  • gltf-embedded
    在这里插入图片描述
gLTF

默认的模型格式,由模型.gltf模型.bin模型.png组成

.gltf文件实际上存储的是json数据,其中包括摄像机、灯光、场景、材质、物体变换等信息,但是不包括几何体纹理信息

.bin文件通常包含了几何体数据,比如每个顶点的位置、UV坐标、法线、颜色等信息

.png则是纹理图片

gLTF-Binary

通常只有一个.glb文件,其中包含了一个模型的所有信息,它是二进制的,体积相对更小;因为只有一个文件,所以导入起来比较方便,但是想要更改其中的数据就比较麻烦了

gLTF-Draco

gLTF本身类似,但是它 使用了Draco算法对缓冲数据进行压缩,所以它的体积相对于gLTF更小

gLTF-Embedded

也是只有一个文件,但是它相当于将gLTF里的所有数据放到了一个文件中(包括纹理图片),通常不会使用这种格式

综上,如果想要修改模型中的数据,最好使用默认的gLTF格式,如果想只是用一个文件来实现,那使用gLTF-Binary比较合适

目前gLTF格式的模型正在逐渐成为标准,它内部使用的材质通常为PBR材质(Physically Based Realistic),PBR材质也会逐渐成为标准的使用材质

导入模型

需要使用对应的gltfLoader来导入,threejs本身也不带有这个loader,所以要从它的仓库里导入:

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltfLoader = new GLTFLoader()
gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    }
)

控制台可以看到导入成功回调里打印的模型信息:
在这里插入图片描述
现在我们要导入的是鸭子的模型,但是这个模型的位置在scene.children[0].children[0]的位置,除了这个模型之外,还存在一个摄像机
在这里插入图片描述
现在我们试着只将该模型导入:

const gltfLoader = new GLTFLoader()

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    scene.add(gltf.scene.children[0].children[0])
    }
)

导入之后发现,模型特别大 直接超出了整个场景:
在这里插入图片描述
回到控制台可以看到,其实在模型的外部Object3D是有设置缩放的:
在这里插入图片描述
并且你会发现,children数组里模型从json里的场景被添加到我们页面的场景后,json数据里的模型就不在了

所以,我们实际情况下可以根据需要,判断是只需要导入模型还是模型原本所处的场景(毕竟场景本身也是继承自Object3D的,同样也可以被添加到threejs的场景中)

修改代码,改成导入整个场景

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    scene.add(gltf.scene)
    }
)

现在就正常了:在这里插入图片描述
当然,也可以自己手动给模型设置缩放,导入的效果和上面一致:

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    const model = gltf.scene.children[0].children[0]
    model.scale.set(0.009, 0.009,0.009)
    scene.add(model)
    }
)

Draco压缩

Draco压缩是一种用于3D图形的数据压缩算法,主要用于3D模型的传输和存储,它由google开发而来,开源

对于一个非常大的模型,如果在浏览器主线程中进行加载或者对draco模型解压,可能会导致浏览器卡上几秒无法操作

所以,threejs提供了dracoLoader,以此实现在worker中进行这部分加载,避免阻塞主线程

首先将gltfLoader加载的模型路径定位到glTF-Draco目录下

gltfLoader.load(
    '/models/Duck/glTF-Draco/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    scene.add( gltf.scene)
    }
)

此时会发现控制台报错,没有找到DracoLoader
在这里插入图片描述
参照官方文档的示例,需要引入DracoLoader以及设置Draco解码器的路径:
在这里插入图片描述
但是按照官方这样子写控制台会报错,提示找不到draco的解码器:
在这里插入图片描述
很显然,因为项目中的相对路径命中的是静态文件目录static,所以找不到对应的解码器

所以这里需要我们去到对应的解码器目录中将整个draco文件夹拷贝到我们的静态目录里:

在这里插入图片描述
然后,将路径修改为我们的静态目录路径即可:

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/') // 指向静态目录
dracoLoader.load(
	'/models/Duck/glTF-Draco/Duck.gltf',
	(gltf)=> {
		console.log('gltf', gltf)
		scene.add( gltf.scene)
	}
)

但是,dracoLoader本身只支持.drc后缀的模型文件,我们的gLTF-Draco目录下的模型文件依然是gltf,所以按照官方示例中的引入方式是不行的,控制台会报错:
在这里插入图片描述
所以,这里我们依然需要使用原本的gltfLoader,只不过为其添加一个DracoLoader

const gltfLoader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
gltfLoader.setDRACOLoader(dracoLoader)

gltfLoader.load(
    '/models/Duck/glTF-Draco/Duck.gltf',
    (gltf) => {
    console.log('gltf', gltf)
    scene.add( gltf.scene)
    }
)

这样子导入经过draco压缩的模型就没问题了

Draco压缩并不是一个十全十美的方案,对于一个只有100KB的模型,经过draco压缩后也才仅仅缩减至50KB,为了用户少加载这50KB的资源,我们做了:

  1. 引入DracoLoader
  2. draco解码器导入到根目录
  3. 在浏览器内花费一定的时间进行解压缩

实际场景中,我们还是应该按需使用这个性能优化方案

如果需要对普通的模型进行draco压缩,可以通过gltf-pipeline工具来实现:https://github.com/CesiumGS/gltf-pipeline

动画

gltf还可以存储模型的动画,当使用gltfLoader导入一个带有动画的模型时,控制台打印该模型数据会发现里面有一些AnimationClip

比如导入项目里的一个Fox模型:
在这里插入图片描述

gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) => {
    	console.log('gltf', gltf)
    	gltf.scene.scale.set(0.025, 0.025,0.025)
    	scene.add( gltf.scene)
    }
)

打印会看到animations数组中存在几段已有的动画片段:
在这里插入图片描述
AnimationClipthreejs里内置的动画片段类,一个clip就代表一段动画

动画代表的是某个物体的动画,所以需要使用一个动画播放器(AnimationMixer)将动画与物体关联起来

然后使用AnimationMixerAnimationClip转为一个动画行为(AnimationAction),调用AnimationActionplay方法,同时在每一帧中更新动画播放器即可实现动画的播放

这是官方文档中给的使用示例:
在这里插入图片描述
在我们项目中,参照示例代码可以写成下面这样的动画实现:

let mixer = null
gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) => {
    // 实例化动画播放器
    mixer = new THREE.AnimationMixer(gltf.scene)
    // 创建动画行为
    const action= mixer.clipAction(gltf.animations[2])
    // 让动画进入播放状态
    action.play()
    gltf.scene.scale.set(0.025, 0.025,0.025)
    scene.add( gltf.scene)
    }
)


const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - previousTime
    previousTime = elapsedTime

    // 在每一帧更新动画播放器
    mixer && mixer.update(deltaTime)
    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

在这里插入图片描述

除了基本的动画外,threejs-AnimationAction还支持动画之间的交叉淡化过渡,可以平滑地过渡两个动画

切换动画时,需要先停止上一个动画,再启动下一个动画,否则会出现两个动画混合在一起的情况

先看直接切换的情况:

let mixer = null
const animationsMap = {}
let currentAction = null
gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) => {
    mixer = new THREE.AnimationMixer(gltf.scene)
    gltf.animations.forEach((animation) => {
        animationsMap[animation.name] = () => {
            const newAction = mixer.clipAction(animation)
            const oldAction = currentAction
            oldAction.stop()
            
            currentAction = newAction
            currentAction.reset()
            currentAction.play()
        }
        gui.add(animationsMap, animation.name)
    })
    currentAction = mixer.clipAction(gltf.animations[0])
    currentAction.play()
    }
)

在这里插入图片描述
官方文档中提供的过渡方法如下:
在这里插入图片描述
所以如果要从旧动画过渡到新动画,这样写就可以了:

        animationsMap[animation.name] = () => {
            const newAction = mixer.clipAction(animation)
            const oldAction = currentAction
            // oldAction.stop() // 过渡动画时不需要停止旧动画
            
            currentAction = newAction
            currentAction.reset()
            currentAction.play()
            // duration的时间单位是秒
            currentAction.crossFadeFrom(oldAction, 0.5)
        }

在这里插入图片描述

调试

对于模型而言,在threejs里用代码进行调试的效率是很低的,threejs提供了一个threejs/editor在线编辑器来帮助我们调试

打开编辑器官网:https://threejs.org/editor/,点击顶栏的文件 -> 新建项目 -> 空清空整个场景,然后将模型文件夹下的文件全部选中并拖入编辑器,就可以看到对应的模型内容
在这里插入图片描述

模型默认是PBR材质的,要让他显示需要外部光源,点击顶栏的添加 -> 光源 -> 环境光,然后在右侧操作面板将光的颜色改为白色,亮度也做适当地调整就可以看到正确的狐狸模型了

demo地址

https://github.com/JohnWicc/threejs-demo/tree/21-import-models

环境贴图

使用THREE.CubeTextureLoader进行加载,通常加载的环境贴图格式为px、nx、py、ny、pz、nz的六张图

const environmentMap = cubeTextureLoader.load([
    '/environmentMaps/0/px.png',
    '/environmentMaps/0/nx.png',
    '/environmentMaps/0/py.png',
    '/environmentMaps/0/ny.png',
    '/environmentMaps/0/pz.png',
    '/environmentMaps/0/nz.png'
])

通过给场景background设置envMap可以直接让背景变成类似360度环绕的盒子:

scene.background = environmentMap

效果如图:
在这里插入图片描述
目前场景中有一个导入的模型和一个TorusKnot几何体,他们都是使用的MeshStandardMaterial,目前环境没有光照,所以他们均为黑色

通过给scene.enviroment设置envMap则可以为场景中所有的物体添加上该envMap

scene.enviroment = enviromentMap

这是官网的描述:
在这里插入图片描述
效果:
在这里插入图片描述
可以看到,给一个MeshStandardMaterial材质的物体加上环境贴图后,他就会自己具有一定的亮度了

调整环境强度

目前场景中有两个物体,一个是模型,一个是内置的几何体,我们已经通过设置sceneenviromentMap给他们加上了环境贴图,但是他们看起来"太暗了"

我们可以通过给场景添加外部光源来提亮,但是在这种情况下,我们也可以通过设置物体材质的envMapIntencity来调整其对环境贴图反应的强度,以此来提高亮度,这样才是比较好的处理方式

我们可以给每个物体单独设置envMapIntensity,但是如果场景里物体很多的话,一个一个设置效率就太低了。threejs中提供了一个traverse遍历方法,来实现对继承自Object3D类的物体遍历

接下来实现一个对场景内的物体进行遍历的函数,对每一个MeshStandardMaterial材质的物体设置其对环境的响应程度,并且在模型加载完成时调用他:

const updateAllMaterials = () => {
    scene.traverse((child) => {
        // 通常会使用 child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial来判断
        // 但是threejs里提供了一些捷径式的判断属性:
        if (child.isMesh && child.material.isMeshStandardMaterial) {
            // child.material.envMap = environmentMap
            child.material.envMapIntensity = 3
        }
    })
}

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    gltf => {
        console.log('gltf:', gltf)
        gltf.scene.scale.set(10,10,10)
        scene.add(gltf.scene)
        updateAllMaterials()
    }
)

这样,我们就不需要添加额外光源来让整体的亮度提高了:
这样

数值调制

在代码里修改updateAllMaterials方法中的envMapIntensity数值还是不够方便,为了提高效率我们还是应该结合lil-gui进行调试

声明一个全局envMapIntensity变量,通过lil-gui监听该变量的修改来调用updateAllMaterials方法对材质的环境响应程度进行更新:

const global = {};
global.envMapIntensity = 1;

const updateAllMaterials = () => {
  scene.traverse((child) => {
    if (
      child instanceof THREE.Mesh &&
      child.material instanceof THREE.MeshStandardMaterial
    ) {
      // child.material.envMap = environmentMap
      child.material.envMapIntensity = global.envMapIntensity;
    }
  });
};
gui
  .add(global, "envMapIntensity")
  .min(0)
  .max(10)
  .step(0.001)
  .onChange(updateAllMaterials);

背景调制

通过scene.backgroundBlurriness可以设置背景的模糊程度

scene.backgroundBlurriness = 0.2;

在这里插入图片描述
假如使用了一张不是很清晰的envMap,那么可以添加适当的模糊来让用户只关注在物体上

通过scene.backgroundIntensity则可以设置背景的亮度:

scene.backgroundIntensity = 5

在这里插入图片描述

hdr格式文件

对于上面同样的环境贴图,hdr格式的文件长这样:
在这里插入图片描述

.hdr代表的是 High Dynamic Range,意味着其能存储更精确的颜色数据,正常的图片通常存储R、G、B颜色本身有哪些,但是HDR更进一步地将R、G、B的数值也存储了下来

所以,要在threejs里加载hdr格式的文件贴图,则需要使用RGBELoader
RGBE代表的是Red Green Blue ExponentRGB指数,这个loader官方文档中是没有记述的

import {  RGBELoader } from "three/examples/jsm/Addons.js";
const rgbeLoader = new RGBELoader()
rgbeLoader.load("/environmentMaps/0/2k.hdr", (envMap) => {
    envMap.mapping = THREE.EquirectangularReflectionMapping
    console.log('envMap', envMap)
    scene.background = envMap;
    scene.environment = envMap;
})

注意这里还需要对加载出来的envMap设置mapping,告诉threejs要使用什么投影算法来计算
在这里插入图片描述
假如不添加envMap.mapping = THREE.EquirectangularReflectionMapping,得到的结果就是:
在这里插入图片描述
添加之后,整体不需要调整亮度就已经很亮了:
在这里插入图片描述
可以和基本的环境贴图版本做一下对比:
在这里插入图片描述
使用hdr一个是基本的亮度有所提升,另一个是hdr格式的环境贴图割裂感不会有那么强,比如上面红色框选的部分

但是显然,随着.hdr文件而来的代价就是其体积较大,加载性能肯定不如cubeTexture,所以还是一样的要根据实际需要进行取舍

一个找高清环境贴图的网站

https://polyhaven.com/

HDR转为cube map的网站:

https://matheowis.github.io/HDRI-to-CubeMap/

自己创建环境贴图(使用blender)

从上面的案例可以看到,环境贴图除了当作一个“图”之外,它还可以创造出光源的效果,所以,除了使用已有的环境贴图,也可以自己使用blender等3D软件创造出一个环境贴图:

例如,现在打开blender4.2,进行如下设置:
修改右下角的渲染引擎,改为Cycles,设置采样 -> 视图采样 -> 渲染的采样率为256(默认的是1024,但是笔者电脑没那么强,所以要适当降低)
在这里插入图片描述
然后去到输出中,将输出分辨率改为2048 x 1024(2的指数幂)
在这里插入图片描述
来到世界环境,在"表面"添加节点,设置世界背景为黑色(这一步是为了让整个场景的背景保持为黑色,方便看出光源的颜色)
在这里插入图片描述
然后在场景中添加一个面光,调整其为适当的大小,并放置在合适的位置:
在这里插入图片描述
在右下角的“物体"菜单,将射线可见性 -> 摄像机勾选
在这里插入图片描述
默认情况下,在blender中添加的光源实体在最后的渲染阶段是不会出现在摄像机内的,所以需要在灯光的可见性中进行设置

添加完一个面光后将其进行复制,在四个方位分别放置一个,并且设置光的颜色为不同的颜色,同时将这些光的强度设置为1000kw
在这里插入图片描述
最后,往场景中添加一个”全景摄像机“,选择全景类型为等距矩形,将其初始位置和旋转角度全部置为0:
在这里插入图片描述
然后就可以进行渲染了,按F12,进入渲染界面,会看到最终要渲染出来的结果长这样,这就是一个360度的柱状全景图被展开成平面图的样子

在这里插入图片描述
和前例中的.hdr格式文件是一样的:
在这里插入图片描述
在渲染界面按ALT + S将其保存为图片,保存时选择HDR模式存储
在这里插入图片描述
然后就可以在项目中导入刚刚获得的.hdr文件了,在当前场景下导入我们自己的环境贴图效果为:
在这里插入图片描述
我们现在是把场景的background也设置了这个环境贴图,把他去掉就可以只留下场景中的两个模型,并且也同时具有光照效果了:

rgbeLoader.load("/environmentMaps/pure-light-env2.hdr", (envMap) => {
    envMap.mapping = THREE.EquirectangularReflectionMapping
    // scene.background = envMap;
    scene.environment = envMap;
})

在这里插入图片描述
所以,实际上所谓的环境贴图达成的目的是模拟灯光、阴影等真实世界的效果,你的环境贴图和背景图片可以完全不同;当然你也可以自己在threejs里创建4个RectLight来实现打光的效果,但是论开发的效率和运行在网站上的性能当然就不如环境贴图了

压缩hdr

一个好的,高清的.hdr通常动辄几M,使用这么"大"的环境贴图对于用户的体验也是不友好的

而所谓的压缩hdr,实际上是将其转换为cubeMap格式的图片来实现环境贴图,通常这会牺牲环境光的质量换来更快的加载速度

比如对我们刚刚自己创建的.hdr文件进行压缩,打开前文提到的网站:
https://matheowis.github.io/HDRI-to-CubeMap/
上传我们的HRD文件:
在这里插入图片描述
选择cubeMapView进行导出,格式选择最下面的6张图的格式:
在这里插入图片描述
process并保存后,会得到一个压缩文件,将其解压就可以得到6张标准的CubeMap图片,这样就可以用threejs内置的CubeMapTextureLoader来加载了
在这里插入图片描述
加载的代码:

const environmentMap = cubeTextureLoader.load([
  "/environmentMaps/Standard-Cube-Map/px.png",
  "/environmentMaps/Standard-Cube-Map/nx.png",
  "/environmentMaps/Standard-Cube-Map/py.png",
  "/environmentMaps/Standard-Cube-Map/ny.png",
  "/environmentMaps/Standard-Cube-Map/pz.png",
  "/environmentMaps/Standard-Cube-Map/nz.png",
]);
scene.background = environmentMap;
// scene.backgroundBlurriness = 0.2;
// scene.backgroundIntensity = 5
scene.environment = environmentMap;
// rgbeLoader.load("/environmentMaps/pure-light-env2.hdr", (envMap) => {
//     envMap.mapping = THREE.EquirectangularReflectionMapping
//     // console.log('envMap', envMap)
//     // scene.background = envMap;
//     scene.environment = envMap;
// })

最终的效果:
在这里插入图片描述
效果显然没有.hdr来的要好,并且环境贴图的质量也掉的很严重,初始的光照效果远远偏离了我们设置的1000kw的光照强度,但是我们也可以通过修改物体材质的envMapIntensity来控制其亮度,修改后其实达成的光照效果也能和使用hdr时一致:
在这里插入图片描述

其他获取环境贴图的方式

NVIDIA CANVAS

英伟达提供的AI生成式环境贴图,简单来说就是用户随意画画,AI生成对应的场景,需要安装他们的软件
在这里插入图片描述
https://www.nvidia.com/zh-tw/studio/canvas/

这个软件目前还处于测试阶段,使用它导出的图片是.exr格式,这个格式的文件也是需要在threejs中使用对应的loader进行加载

import { EXRLoader, GLTFLoader, RGBELoader } from "three/examples/jsm/Addons.js";

const exrLoader = new EXRLoader()
exrLoader.load("/environmentMaps/nvidiaCanvas-4k.exr", (envMap) => {
    envMap.mapping = THREE.EquirectangularReflectionMapping
    scene.background = envMap;
    scene.environment = envMap;
})

在这里插入图片描述

AI生成skybox

AI生成的环境贴图,根据提示词进行生成,生成出来的也是柱状全景的环境贴图
在这里插入图片描述

不过生成后下载需要付费。。
在这里插入图片描述

网站:https://skybox.blockadelabs.com/

加载jpg

实际上柱状全景图也可以导出成jpg格式的图片进行加载,比如使用项目里的一张jpg格式的全景图:

在这里插入图片描述

threejs中使用基本的textureLoader进行加载:

const texture = textureLoader.load('/environmentMaps/blockadesLabsSkybox/digital_painting_neon_city_night_orange_lights_.jpg')
texture.mapping = THREE.EquirectangularReflectionMapping
scene.background = texture;
scene.environment = texture;

得到的效果和原图有点区别,这是因为threejs里一般的texture默认色彩空间是线性的色彩空间THREE.LinearSRGBColorSpace

在这里插入图片描述

需要多加一句,修改色彩空间模式:

texture.colorSpace = THREE.SRGBColorSpace

这样得到的效果就对味儿了,亮度不够的话就通过右上角调整即可:

在这里插入图片描述

综上所述,到目前为止,环境贴图的作用是实现打光,threejs中的材质可以根据需要设置受光的强度,相比于在threejs中创造多个不同的光源,使用环境贴图来实现场景光对于性能更加友好;同时,环境贴图的格式有很多,根据不同的需求,选择不同的贴图格式也是比较重要的

使用时注意事项

  1. 如果在mesh被添加到场景后才加载完envMap,那你可能需要手动通知material进行更新:
scene.add(mesh)

setTimeout(() => {
  mesh.material.envMap = envMapTexture // 模拟环境贴图延迟加载的场景
  mesh.material.needsUpdate = true // 手动通知threejs更新贴图
}, 1000)

// 假如场景中有多个mesh需要更新,那也可以遍历所有物体进行加载
textureLoader.load(textureUrl, (texture) => {
  scene.traverse(child => {
    if (child instanceof THREE.Mesh && child.material.isMeshStandardMaterial) {
	  child.material.envMap = texture
	  child.material.envMapIntensity = 2
	  child.material.needsUpdate = true // 记得要手动通知
	}
  })
})
  1. 如果envMap已经加载完成,后续再添加一个新的mesh,那么就需要重新调用一次mesh.envMap = envMapTexture的设置,否则物体不会受已经添加到场景中的envMap影响

demo地址

https://github.com/JohnWicc/threejs-demo/tree/enviroment-map

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值