theme: smartblue
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
摘要
本节专栏内容,我们来了解一下 Three.js
中的多媒体知识,文章将依次详细讲解 Three.js
中文本字体的原理的应用、图片元素原理及应用、音频元素的原理及应用、视频元素的原理及应用等。了解完基本原理后,将利用本文内容所学到的知识,简单制作一个可以播放视频的三维手机展示页面。通过本文内容的学习和实践,你将了解到 Three.js
中各种多媒体元素的应用场景及何使用多媒体元素创建一个渲染更加真实的三维可交互页面相关的内容。
效果
在学习原理之前,我们先来看看本文多媒体应用的最终示例页面,它是一个三维产品展示页面,主体是最新版某知名品牌手机 📱
,场景背景使用了全景天空盒,使用鼠标可以对三维场景进行旋转和缩放、点击手机屏幕开始播放视频,再次点击屏幕视频停止,屏幕还原。由于示例页面部署在 Gitpage
,可能会有加载超时的情况,大家感兴趣的话最好克隆代码到本地实践。
打开以下链接,在线预览效果,大屏访问效果更佳。
👁🗨
在线预览地址:https://dragonir.github.io/threejs-odessey
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
码上掘金
原理
文字
在三维页面开发过程中,3D
字体是我们需要经常用到的页面装饰和功能介质,我们可以通过如下简单的方式,在 Three.js
中添加文本字体,其中 FontLoader
用于加载字体文件;TextGeometry
用于创建字体网格。
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';
fontLoader.load('fontface.json', font => {
textMesh.geometry = new TextGeometry('1000!', {
font: font,
size: 100,
height: 40
});
scene.add(textMesh);
});
💡
字体加载器 FontLoader
FontLoader
是用于加载 JSON
格式的字体的类,返回 font
, 返回值是表示字体的 Shape
类型的数组。
构造函数:
FontLoader(manager : LoadingManager)
方法:
.load (url : String, onLoad : Function, onProgress : Function, onError : Function) : undefined
url
:文件的URL
或者路径,也可以为Data URI
。onLoad
:将在加载完成时调用。参数是将要被加载的font
。onProgress
:将在加载过程中调用,参数是包含total
和loaded
字节值。onError
:将在加载错误时调用。
.parse (json : Object) : Font
json
:用于解析的JSON
格式的对象。
💡
文本缓冲几何体 TextGeometry
TextGeometry
用于将文本生成为单一的几何体的类,它是由一串给定的文本以及由加载的字体和该几何体父类中的设置参数来构成。
构造函数:
TextGeometry(text : String, parameters : Object)
text
:将要显示的文本。parameters
:包含有下列参数的对象font
:THREE.Font
的实例。size[Float]
:字体大小,默认值为100
。height[Float]
:挤出文本的厚度。默认值为50
。curveSegments[Integer]
:表示文本曲线上点的数量。默认值为12
。bevelEnabled[Boolean]
:是否开启斜角,默认为false
。bevelThickness[Float]
:文本上斜角的深度,默认值为20
。bevelSize[Float]
:斜角与原始文本轮廓之间的延伸距离,默认值为8
。bevelSegments[Integer]
:斜角的分段数,默认值为3
。
如果我们只想单纯在页面上加载文案介绍标签,而不是三维字体的话,我们直接使用 HTML
元素和 CSS
实现,可以利用 raycaster
来实现文字在三维场景中的显示与隐藏。具体实现方法可以看看我之前写的另一篇文章《Three.js 打造缤纷夏日3D梦中情岛》。
🔗
在线将字体转换为Three.js支持的格式:facetype
图片
图片在 Three.js
中也是经常用到的元素,比如我们可以使用图片给网格模型添加纹理、可以使用图片给场景添加背景或天空盒子、也可以单纯用于信息展示。下面列举了几种在场景中加载图片的方法,我们可以根据不同的场景选择合适的方法。
常用的方法,我们在场景中创建一个平面网格,然后使用 TextureLoader
加载图片,将图片作为平面的纹理贴图添加到场景中。
let mesh = new THREE.Mesh(new THREE.PlaneGeometry(10.41, 16), new THREE.MeshStandardMaterial({
map: new THREE.TextureLoader().load('/texture/image.png'),
transparent: true,
side: THREE.DoubleSide
}));
💡
材质加载器 TextureLoader
TextureLoader
是 Three.js
中加载图片材质的一个类,其内部使用 ImageLoader
来加载文件。
构造函数:
TextureLoader(manager :LoadingManager)
方法:
.load (url: String, onLoad: Function, onProgress: Function, onError: Function) :Texture
url
:文件的URL
或者路径。onLoad
:加载完成时将调用,回调参数为将要加载的texture
。onProgress
:将在加载过程中进行调用,实例包含total
和loaded
字节。onError
:在加载错误时被调用。
第二个经常应用到图片的场景就是图片可以作为场景的背景,除了将渲染器设置透明在 CSS
中设置 background
的方式之外,我们也可以直接通过 scene.background
方法来给场景设置背景。更加炫酷的方式是,我们可以通过如下的方式给场景设置三维全景环境贴图,效果类似于全景照片或者全景地图。本文后续的示例中,也将采用该方法给页面设置全景背景。
const cubeTextureLoader = new THREE.CubeTextureLoader();
const environmentMap = cubeTextureLoader.load([
'/textures/environmentMaps/px.jpg',
'/textures/environmentMaps/nx.jpg',
'/textures/environmentMaps/py.jpg',
'/textures/environmentMaps/ny.jpg',
'/textures/environmentMaps/pz.jpg',
'/textures/environmentMaps/nz.jpg'
]);
scene.background = environmentMap;
scene.environment = environmentMap;
💡
立方体材质加载器 CubeTextureLoader
CubeTextureLoader
是用于加载立方体材质的一个类,内部使用 ImageLoader
加载文件。
构造函数:
CubeTextureLoader(manager :LoadingManager)
方法:
.load ( urls : String, onLoad : Function, onProgress : Function, onError : Function ) : null
urls
:数组长度为6
的图像数组,内容为URL
,每一个URL
用于CubeTexture
的每一侧,顺序为[pos-x, neg-x, pos-y, neg-y, pos-z, neg-z]
。onLoad
:加载完成时将调用,回调参数是已被加载的texture
。onProgress
:将在加载过程中进行调用,包含total
和loaded
字节。onError
:在加载错误时被调用。
下面两个网站提供丰富的三维全景背景照片及将 hdr
图片裁切成上述需要的 6
张贴图的能力,大家可以按自己需要下载和编辑。
🔗
HDR全景背景照片下载网站:polyhaven
🔗
HDR立方体材质转换工具:HDRI-to-CubeMap
除此之外,如我们在本专栏《Three.js 进阶之旅:神奇的粒子系统-迷失太空》一文中所学的一样,还可以将图片转化为 canvasTexture
加载到模型中。
音频
Three.js
中音频元素可以用于游戏音效或者数字展厅等场景中。现在,我们介绍关于声音的两个对象。Three.js
中声源有一个非常有趣的特性就是生源会受到摄像机距离的影响,当摄像机离声源物体距离较近时,音量就会变大,反之则会变小;摄像机左右两侧的位置分别决定着左右两侧扬声器声音的大小。
我们像下面这个示例一样,在场景中添加音频。首先,我们在场景中先创建一个 THREE.PersoectiveCamera
,然后定义 THREE.AudioListener
对象,并将它通过 add
方法添加到相机上。为了体验音频的上述特性,我们在场景中创建 3
个音源:
var listener1 = new THREE.AudioListener();
var listener2 = new THREE.AudioListener();
var listener3 = new THREE.AudioListener();
camera.add(listener1);
camera.add(listener2);
camera.add(listener3);
接着,我们在场景中创建一个立方体作为音频的载体,并创建一个 THREE.PositionalAudio
对象关联到 THREE.AudioListener
对象上,最后加载音频文件,并在加载器内通过设置一些音频属性来控制音频的播放和表现,其中:
setRefDistance
:该属性指定声音从距离声源多远的位置开始衰减其音量。setLoop
:该属性指定声音是否被循环播放,默认只播放一遍。setRolloffFactor
:该属性指定声源音量随着距离衰减的速度。
var geometry = new THREE.BoxGeometry(40, 40, 40);
var material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: textureLoader.load('/texture/dog.png')
});
var dog = new THREE.Mesh(geometry, material);
dog.position.set(0, 20, 0);
var posSound = new THREE.PositionalAudio(listener1);
var audioLoader = new THREE.AudioLoader();
audioLoader.load('/audio/dog.ogg', function (buffer) {
posSound.setBuffer(buffer);
posSound.setRefDistance(30);
posSound.play();
posSound.setRolloffFactor(10);
posSound.setLoop(true);
});
这样我们将在场景中添加了一个可以播放狗叫声音的 dog
声源,利用相同的步骤,我们再往场景中 (0, 20, 100)
和 (0, 20, -100)
的位置再添加其他两种动物的声源。然后我们再往场景中添加一个第一人称控制器 THREE.FirstPersonControls
,这样就能模拟在场景中来回走动的效果。在移动的过程中,就能感受到上述的音频在 Three.js
中场景中的特性,声音的的音量和位置会随着摄像机所在的位置变化。例如,如果你将视角位置移动到奶牛的正前方,你就基本上只能听到奶牛的声音,而猫和狗的声音则位于左声道中且非常微弱。
controls = new THREE.FirstPersonControls(camera);
controls.movementSpeed = 70;
controls.lookSpeed = 0.15;
controls.noFly = true;
controls.lookVertical = false;
💡
AudioListener
AudioListener
用一个虚拟的 listener
表示在场景中所有的位置和非位置相关的音效。
一个 Three.js
程序通常创建一个 AudioListener
,它是音频实体构造函数的必须参数。大多数情况下, listener
对象是 camera
的子对象,Camera
的 3D
变换表示了 listener
的 3D
变换。
构造函数:
AudioListener()
属性:
.context[AudioContext]
:listener
构造函数中的AudioContext
。.gain[GainNode]
:使用AudioContext.createGain()
创建GainNode
。.filter[AudioNode]
:默认为null
。.timeDelta[Number]
:audio
实体的时间差值,默认是0
。
方法:
.getInput()
:返回gainNode
。.removeFilter()
:设置filter
属性为null
。.getFilter()
:返回filter属性的值。.setFilter(value: AudioNode)
:设置filter
属性的值。.getMasterVolume()
: 返回音量。.setMasterVolume(value: Number)
:设置音量。
💡
音频加载器 AudioLoader
用来加载 AudioBuffer
的一个类,内部默认使用 FileLoader
来加载文件。
构造函数:
AudioLoader(manager: LoadingManager)
方法:
.load(url: String, onLoad: Function, onProgress: Function, onError: Function): undefined
url
:文件的URL
或者路径。onLoad
:加载完成时将调用,回调参数为将要加载的响应文本。onProgress
:将在加载过程中进行调用,实例包含total
和loaded
字节。onError
:在加载错误时被调用。
💡
PositionalAudio
PositionalAudio
创建一个位置相关的音频对象。
构造函数:
PositionalAudio(listener :AudioListener);
属性:
.panner[PannerNode]
:位置相关音频的 PannerNode
。
方法:
.getOutput()
:返回panner
。.getRefDistance()
:返回panner.refDistance
的值。.setRefDistance(value: Float)
:设置panner.refDistance
的值。.getRolloffFactor ()
:返回panner.rolloffFactor
的值。.setRolloffFactor (value: Float)
:设置panner.rolloffFactor
的值。.getDistanceModel()
:返回panner.distanceModel
的值。.setDistanceModel(value: String)
:设置panner.distanceModel
的值。.getMaxDistance()
:返回panner.maxDistance
的值。.setMaxDistance(value : Float)
:设置panner.maxDistance
的值。.setDirectionalCone(coneInnerAngle: Float, coneOuterAngle: Float, coneOuterGain: Float )
:用来把环绕声音转换为定向声音。
💡
第一人称控制器 FirstPersonControls
在音频例子中,镜头控制器使用的是第一人称控制器 FirstPersonControls
,它是 FlyControls
的另一个实现。
构造函数:
FirstPersonControls(object : Camera, domElement : HTMLDOMElement)
object
: 被控制的摄像机。domElement
: 用于事件监听的HTML
元素。
属性:
.activeLook[Boolean]
:是否能够环视四周,默认为true
。.autoForward[Boolean]
:摄像机是否自动向前移动,默认为false
。.constrainVertical[Boolean]
:垂直环视是否约束在[.verticalMin, .verticalMax]
之间,默认值为false
。.domElement[HTMLDOMElement]
:用于监听鼠标/触摸事件,该属性必须在构造函数中传入。.enabled[Boolean]
:是否启用控制器,默认为true
。.heightCoef
:确定当y
分量接近.heightMax
时相机移动的速度,默认值为1
..heightMax[Number]
:用于移动速度辅助的摄像机高度上限,默认值为1
。.heightMin[Number]
:用于移动速度判定的相机高度下限,默认值为0
。.heightSpeed[Boolean]
:摄像机的高度是否影响向前移动的速度,默认值为false
。.lookVertical[Boolean]
:是否能够垂直环视,默认为true
。.lookSpeed[Number]
:环视速度,默认为0.005
。.mouseDragOn[Boolean]
:鼠标是否被按下,只读。.movementSpeed[Number]
:移动速度,默认为1
。.object[Camera]
:被控制的摄像机。.verticalMax[Number]
:你能够垂直环视角度的上限,范围在0
到Math.PI
弧度之间,默认为Math.PI
。.verticalMin[Number]
:你能够垂直环视角度的下限,范围在0
到Math.PI
弧度之间,默认为0
。
方法:
.dispose()
:若不再需要该控制器调用此方法。.handleResize()
:若应用程序窗口大小发生改变时调用此方法。.lookAt(vector: Vector3)
或.lookAt (x : Float, y: Float, z: Float)
:一个表示目标位置的向量,或者世界空间位置的x
、y
、z
分量。.update(delta: Number)
:更新控制器,常被用在动画循环中。
🚩
音频应用的完整示例代码位于08-media/src/sampleAudio.js
,若想了解详细实现细节可查看该文件。
视频
在 Three.js
中自然也可以使用视频媒介来展示内容,我们可以使用视频元素制作数字展厅、产品介绍简介、元宇宙会议等。我们可以像下面这样使用视频元素:
const video = document.getElementById('video');
const texture = new THREE.VideoTexture(video);
视频纹理 VideoTexture
VideoTexture
用于创建一个使用视频来作为贴图的纹理对象。
构造函数:
VideoTexture(video: Video, mapping: Constant, wrapS: Constant, wrapT: Constant, magFilter: Constant, minFilter: Constant, format: Constant, type: Constant, anisotropy: Number)
video
:将被作为纹理贴图来使用的Video
元素。mapping
:纹理贴图将被如何映射到物体上,它是THREE.UVMapping
中的对象类型。wrapS
:默认值是THREE.ClampToEdgeWrapping
。wrapT
:默认值是THREE.ClampToEdgeWrapping
。magFilter
:当一个纹素覆盖大于一个像素时,贴图将如何采样,其默认值为THREE.LinearFilter
。minFilter
:当一个纹素覆盖小于一个像素时,贴图将如何采样,其默认值为THREE.LinearFilter
。format
:默认值为THREE.RGBAFormat
。type
:默认值是THREE.UnsignedByteType
。anisotropy
:沿着轴,通过具有最高纹素密度的像素的采样数。 默认情况下,这个值为1
。设置一个较高的值将会比基本的mipmap
产生更清晰的效果,代价是需要使用更多纹理样本。
属性:
.generateMipmaps[Boolean]
:是否生成mipmap
,默认为false
。.isVideoTexture[Boolean]
:只读,用于检查给定对象是否为视频纹理类型。.needsUpdate[Boolean]
:不必手动设置这个值,它由update()
方法来进行控制。
方法:
.update()
:在每一次新的一帧可用时,这个方法将被自动调用,并将.needsUpdate
设置为true
。
🚩
注意:首次使用纹理后,无法更改视频。相反,在纹理上调用.dispose并实例化一个新的纹理。
实现
接下来,我们应用以上讲述的多媒体知识,制作一个可以播放视频的三维手机展示页面。
场景初始化
场景初始化流程和专栏前面几个示例流程基本上一样的,都是按初始化渲染器、场景、相机、添加光照、页面缩放事件监听更新相机等流程进行,因此不再赘述。为了提高场景渲染的真实性,本文着重关注在 WebGLRenderer
初始化之后设置的这些属性:
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl'),
antialias: true,
alpha: true
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.physicallyCorrectLights = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 2;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.physicallyCorrectLights
:该属性表示是否开启物理光线矫正,开启后可设置随着离光源的距离增加光照如何减弱,点光源和聚光灯等灯光受其影响。将其设置为true
可以使得场景中的光线更自然。renderer.toneMapping
:表示的是渲染场景的色调映射类型,目前Three.js
中包含NoToneMapping
、LinearToneMapping
、ReinhardToneMapping
、CineonToneMapping
、ACESFilmicToneMapping
这5
中色调类型,根据实际的应用场景,可以选择不同的渲染色调来增强渲染真实性。renderer.toneMappingExposure
:是色调映射的曝光程度,值越大,曝光度越高。renderer.outputEncoding
:表示渲染器的输出编码类型,可以告知渲染器将片段着色器中的最终颜色值从线性颜色空间转化为sRGB
颜色空间,Three.js
中默认编码类型为LinearEncoding
,当我们使用的纹理中包含sRGB
数据编码时,我们可以将其设置为sRGBEncoding
,使纹理正确渲染。
创建全景背景
为了营造炫酷的三维效果,我们可以按上述图片章节所讲述的原理将场景背景设置成全景,除了需要将 scene.background
设置成环境贴图之外,我们还需要修改一下 scene
的 environment
属性,这样做的目的是可已将环境贴图应用到场景中的全局对象上,此时不需要通过 scene.traverse
遍历场景的所有对象来一一修改每个对象的环境贴图。
const cubeTextureLoader = new THREE.CubeTextureLoader();
const environmentMap = cubeTextureLoader.load([
'/textures/environmentMaps/px.jpg',
'/textures/environmentMaps/nx.jpg',
'/textures/environmentMaps/py.jpg',
'/textures/environmentMaps/ny.jpg',
'/textures/environmentMaps/pz.jpg',
'/textures/environmentMaps/nz.jpg'
]);
environmentMap.encoding = THREE.sRGBEncoding;
scene.background = environmentMap;
scene.environment = environmentMap;
加载手机模型
加载模型前,我们先创建好视频纹理 videoTexture
,然后使用该纹理创建基于 MeshPhysicalMaterial
类型的 videoMaterial
视频材质。接着使用 GLTFLoader
加载手机模型,并将其添加到场景中。在回调方法中,我们可以像下面这样对手机模型 📱
的屏幕、边框、logo
等对象的材质的金属度、粗糙度等属性进行微调。然后复制一份屏幕的的原始材质,以便于下个步骤中实现视频材质和原始材质的来回切换。
// 创建视频材质
const video = document.getElementById('video');
const videoTexture = new THREE.VideoTexture(video);
// 用于屏幕模型材质切换和点击交互
const screen = {
mesh: null,
material: null,
videoMaterial: new THREE.MeshPhysicalMaterial({
map: videoTexture,
envMap: environmentMap
})
};
// 加载模型
const loader = new GLTFLoader();
let model = null;
loader.load('/models/iphone.glb', mesh => {
if (mesh.scene) {
mesh.scene.traverse(child => {
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
child.material.envMap = environmentMap;
child.material.envMapIntensity = 2;
if (child.name === '屏幕') {
screen.mesh = child;
screen.material = child.material;
}
if (child.name.includes('logo')) {
child.material.metalness = 1;
}
}
})
mesh.scene.scale.set(60, 60, 60);
mesh.scene.position.y = -5;
mesh.scene.rotation.y = -Math.PI;
model = mesh.scene;
scene.add(mesh.scene);
}
});
视频切换效果
上个步骤中,我们已经新建了手机屏幕的视频材质、保存了它的原始材质,并在 screen.mesh
中保存了手机屏幕网格模型。现在我们使用 THREE.Raycaster
通过如下的方法来实现点击手机屏幕切换它的材质功能。监听点击事件,当鼠标所点击区域发出的射线与 [screen.mesh]
网格模型数组相交时,说明点击位置位于手机屏幕上,此时我们就可以通过判断手机屏幕的材质类型来切换加载视频材质还是原始材质。
//声明raycaster和mouse变量
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', event => {
// 通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera(mouse, camera);
// 获取raycaster直线和所有模型相交的数组集合
const intersects = raycaster.intersectObjects([screen.mesh]);
if (intersects.length > 0) {
const mesh = intersects[0].object;
if (mesh.material.type === 'MeshStandardMaterial') {
mesh.material = screen.videoMaterial;
} else {
mesh.material = screen.material;
}
}
}, false);
页面优化
到这里,整个示例就开发完毕了,我们可以给手机模型添加一些自转动画、在页面上增加一些提示语、随着手机旋转的悬浮文字标签等装饰性元素,增强页面的可交互性和用户体验 🌠
。
总结
本文中主要包含的知识点包括:
Three.js
中文本字体的应用,及FontLoader
、TextGeometry
的详细用法。Three.js
中图片元素的应用,及TextureLoader
、CubeTextureLoader
的详细用法。Three.js
中音频元素的应用,及AudioListener
、AudioLoader
、PositionalAudio
的详细用法。Three.js
中视频元素的应用,及VideoTexture
的详细用法。- 第一人称控制器
FirstPersonControls
的用法。 - 如何使用多媒体元素创建一个渲染更加真实的三维可交互页面。
想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 👍。
附录
- [1]. 🌴 Three.js 打造缤纷夏日3D梦中情岛
- [2]. 🔥 Three.js 实现炫酷的赛博朋克风格3D数字地球大屏
- [3]. 🐼 Three.js 实现2022冬奥主题3D趣味页面,含冰墩墩
- [4]. 🦊 Three.js 实现3D开放世界小游戏:阿狸的多元宇宙
- [5]. 🏆 掘金1000粉!使用Three.js实现一个创意纪念页面
...
- 【Three.js 进阶之旅】系列专栏访问 👈
- 更多往期【3D】专栏访问 👈
- 更多往期【前端】专栏访问 👈
参考
- [1]. three.js journey
- [2]. threejs.org
- [3]. 《Three.js 开发指南——基于WebGL和HTML5在网页上渲染3D图形和动画》
暂无评论内容