Three.js 进阶之旅:多媒体应用-3D Iphone 📱


theme: smartblue

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

摘要

本节专栏内容,我们来了解一下 Three.js 中的多媒体知识,文章将依次详细讲解 Three.js 中文本字体的原理的应用、图片元素原理及应用、音频元素的原理及应用、视频元素的原理及应用等。了解完基本原理后,将利用本文内容所学到的知识,简单制作一个可以播放视频的三维手机展示页面。通过本文内容的学习和实践,你将了解到 Three.js 中各种多媒体元素的应用场景及何使用多媒体元素创建一个渲染更加真实的三维可交互页面相关的内容。

效果

在学习原理之前,我们先来看看本文多媒体应用的最终示例页面,它是一个三维产品展示页面,主体是最新版某知名品牌手机 📱,场景背景使用了全景天空盒,使用鼠标可以对三维场景进行旋转和缩放、点击手机屏幕开始播放视频,再次点击屏幕视频停止,屏幕还原。由于示例页面部署在 Gitpage,可能会有加载超时的情况,大家感兴趣的话最好克隆代码到本地实践。

preview.gif

打开以下链接,在线预览效果,大屏访问效果更佳。

本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

码上掘金

代码片段

原理

文字

在三维页面开发过程中,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);
});

sample_font.png

💡 字体加载器 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:将在加载过程中调用,参数是包含 totalloaded 字节值。
    • onError:将在加载错误时调用。
  • .parse (json : Object) : Font
    • json:用于解析的 JSON 格式的对象。

💡 文本缓冲几何体 TextGeometry

TextGeometry 用于将文本生成为单一的几何体的类,它是由一串给定的文本以及由加载的字体和该几何体父类中的设置参数来构成。

构造函数

TextGeometry(text : String, parameters : Object)
  • text:将要显示的文本。
  • parameters:包含有下列参数的对象
    • fontTHREE.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
}));

sample_image.png

💡 材质加载器 TextureLoader

TextureLoaderThree.js 中加载图片材质的一个类,其内部使用 ImageLoader 来加载文件。

构造函数

TextureLoader(manager :LoadingManager)

方法

.load (url: String, onLoad: Function, onProgress: Function, onError: Function) :Texture
  • url:文件的 URL 或者路径。
  • onLoad:加载完成时将调用,回调参数为将要加载的 texture
  • onProgress:将在加载过程中进行调用,实例包含 totalloaded 字节。
  • 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:将在加载过程中进行调用,包含 totalloaded 字节。
  • onError:在加载错误时被调用。

下面两个网站提供丰富的三维全景背景照片及将 hdr 图片裁切成上述需要的 6 张贴图的能力,大家可以按自己需要下载和编辑。

🔗 HDR全景背景照片下载网站:polyhaven

poly.png

🔗 HDR立方体材质转换工具:HDRI-to-CubeMap

edit.png

除此之外,如我们在本专栏《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;

sample_audio.png

💡 AudioListener

AudioListener 用一个虚拟的 listener 表示在场景中所有的位置和非位置相关的音效。
一个 Three.js 程序通常创建一个 AudioListener,它是音频实体构造函数的必须参数。大多数情况下, listener 对象是 camera 的子对象,Camera3D 变换表示了 listener3D 变换。

构造函数

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:将在加载过程中进行调用,实例包含 totalloaded 字节。
  • 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]:你能够垂直环视角度的上限,范围在 0Math.PI 弧度之间,默认为 Math.PI
  • .verticalMin[Number]:你能够垂直环视角度的下限,范围在 0Math.PI 弧度之间,默认为 0

方法

  • .dispose():若不再需要该控制器调用此方法。
  • .handleResize():若应用程序窗口大小发生改变时调用此方法。
  • .lookAt(vector: Vector3).lookAt (x : Float, y: Float, z: Float):一个表示目标位置的向量,或者世界空间位置的 xyz 分量。
  • .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 中包含 NoToneMappingLinearToneMappingReinhardToneMappingCineonToneMappingACESFilmicToneMapping5 中色调类型,根据实际的应用场景,可以选择不同的渲染色调来增强渲染真实性。
  • renderer.toneMappingExposure:是色调映射的曝光程度,值越大,曝光度越高。
  • renderer.outputEncoding:表示渲染器的输出编码类型,可以告知渲染器将片段着色器中的最终颜色值从线性颜色空间转化为 sRGB 颜色空间,Three.js 中默认编码类型为 LinearEncoding,当我们使用的纹理中包含 sRGB 数据编码时,我们可以将其设置为 sRGBEncoding,使纹理正确渲染。

创建全景背景

为了营造炫酷的三维效果,我们可以按上述图片章节所讲述的原理将场景背景设置成全景,除了需要将 scene.background 设置成环境贴图之外,我们还需要修改一下 sceneenvironment 属性,这样做的目的是可已将环境贴图应用到场景中的全局对象上,此时不需要通过 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;

step_0.gif

加载手机模型

加载模型前,我们先创建好视频纹理 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);
  }
});

step_1.png

视频切换效果

上个步骤中,我们已经新建了手机屏幕的视频材质、保存了它的原始材质,并在 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);

step_2.gif

页面优化

到这里,整个示例就开发完毕了,我们可以给手机模型添加一些自转动画、在页面上增加一些提示语、随着手机旋转的悬浮文字标签等装饰性元素,增强页面的可交互性和用户体验 🌠

🔗 源码地址:https://github.com/dragonir/threejs-odessey

总结

本文中主要包含的知识点包括:

  • Three.js 中文本字体的应用,及 FontLoaderTextGeometry 的详细用法。
  • Three.js 中图片元素的应用,及 TextureLoaderCubeTextureLoader 的详细用法。
  • Three.js 中音频元素的应用,及 AudioListenerAudioLoaderPositionalAudio的详细用法。
  • Three.js 中视频元素的应用,及 VideoTexture 的详细用法。
  • 第一人称控制器 FirstPersonControls 的用法。
  • 如何使用多媒体元素创建一个渲染更加真实的三维可交互页面。

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 👍

附录

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容