共计 6978 个字符,预计需要花费 18 分钟才能阅读完成。
申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。
摘要
专栏上篇文章《Three.js 进阶之旅:页面平滑滚动 - 王国之泪》解说并实现了如何应用 R3F
进行页面图片平滑滚动,本文内容在上节的根底上,学习如何应用滚动管制 ScrollControls
来管制模型的的动画播放和相机动画,通过滚动鼠标滚轮或者高低挪动触摸板,来管制模型的动画播放进度或者相机的方位视角 ,从而呈现出惊艳的视觉效果。这种乏味的成果大家在平时浏览一些网页的时候应该常常见到,如一些 3D 产品
介绍页向下滑动鼠标滚轮时产品同时旋转并依据产品的不同视角加载不同文案、或者 3D 数字地球
依据滚轮的挪动间隔转到某个国家或地区、还有一些 个人简历
页面或时间轴页面也常常用到这种成果。通过本文的浏览和案例页面的实现,你将学习到的常识包含:R3F
生态中的 ScrollControls
、Html
、useScroll
、useGLTF
、useAnimations
等组件和办法的根本用法、在 R3F
中加载模型并播放模型骨骼动画、通过滚动管制模型动画播放过程和相机参数、页面元素的一些 CSS
动画及页面整体丝滑滚动动画实现等。
成果
本文案例的实现成果如下图所示,页面主体元素由一个三维模型 🐸🦢
、及底部的 5
页 HTML
页面形成,页面初始加载时模型是静止的,当咱们应用鼠标或触控板或间接拖动页面滚动条时 🖱
,相机镜头📷
从侧面近处平滑过渡到模型侧面远处,模型开始自动播放自带骨骼动画,模型动作依据页面滚动间隔和滚动时速率的大小而不同。当咱们点击页面顶部菜单时,页面会平滑滚动到对应地位,模型也会播放动画。
当页面逆向 👆
滚动时,相机和模型动画也会逆向变动和播放。
文章应用 GIF
可能会造成丢帧或卡顿,能够亲自关上预览链接试试,大屏拜访成果更佳。
👁🗨
在线预览地址:https://dragonir.github.io/dancingDuck/
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
🔗
代码仓库地址:git@github.com:dragonir/threejs-odessey.git
原理
如果用原生 JavaScript
实现滚动动画成果,就须要监听滚动事件和计算滚动间隔。本文还是和上篇文章《hree.js 进阶之旅:页面平滑滚动 - 王国之泪》一样,间接应用封装好的组件 ScrollControls
和 Scroll
来实现,它们的具体用法和原理可返回上篇文章查看。本文中最终实现的页面须要加载模型并播放它自带的骨骼动画,因而用到了以下几个 @react-three/drei
中的组件和 hooks
。
Html
容许咱们将 HTML
内容绑定到场景中的任意对象,它将主动投影到对象上。
<Html | |
as='div' // 包裹元素,默认为 'div' | |
wrapperClass // 包裹元素的类名,默认为 undefined | |
prepend // 画布前面的元素,默认为 false | |
center // 增加 -50%/-50% css 变换,默认为 false | |
fullscreen // 与左上角对齐并填满屏幕,默认为 false | |
distanceFactor={10} // 如果设置该值,子元素将按与相机的间隔进行缩放,默认为 undefined | |
zIndexRange={[100, 0]} // Z 阶范畴,默认为 [16777271, 0] | |
portal={domnodeRef} // 对指标容器的援用,默认为 undefined | |
transform // 若设置 true,将进行 3d 矩阵转换,默认为 false | |
sprite // 渲染为 sprite,仅在转换模式下失效,默认为 false | |
occlude // 遮挡模式,默认为 false,当设置为 blending 时将开启真正混合遮挡 | |
castShadow // 产生暗影 | |
receiveShadow // 接管暗影 | |
// 像 Mesh 一样设置材质 | |
material={<meshPhysicalMaterial side={DoubleSide} opacity={0.1} />} | |
// 笼罩默认定位性能 | |
calculatePosition={(el: Object3D, camera: Camera, size: { width: number; height: number}) => number[]} | |
occlude={[ref]} // 能够为真或 Ref<Object3D>,当为 true 时遮挡整个场景,默认为 undefined | |
onOcclude={(visible) => null} // 可见性批改时的回调,默认为 undefined | |
{...groupProps} // 反对所有 THREE.Group 属性 | |
{...divProps} // 反对所有 HTML DIV 元素属性 | |
> | |
<h1>hello</h1> | |
<p>world</p> | |
</Html> |
Html
能够通过配置 occlude
属性暗藏在几何体前面。当 Html
组件暗藏时,它将在最外部的 div
上设置 opacity
属性,如果须要增加动画成果或者管制过渡成果,能够应用 onOcclude
自定义办法。
<Html | |
occlude | |
onOcclude={set} | |
style={{ | |
transition: 'all 0.5s', | |
opacity: hidden ? 0 : 1, | |
transform: `scale(${hidden ? 0.5 : 1})` | |
}} | |
/> |
useGLTF
它是一个应用 useLoader
和 GLTFLoader
的不便钩子函数,它默认应用 draco 来加载已压缩的模型文件。
useGLTF(url) | |
useGLTF(url, '/draco-gltf') | |
useGLTF.preload(url) |
useAnimations
AnimationMixer 的形象钩子办法,能够像上面这样获取到模型自带的动画信息。
const {nodes, materials, animations} = useGLTF(url) | |
const {ref, mixer, names, actions, clips} = useAnimations(animations) | |
useEffect(() => {actions?.jump.play() | |
}) |
THREE.MathUtils.damp
应用 dt
以相似弹簧的形式从 x
向 y
平滑地插入一个数字,以放弃与帧速率无关的静止。
.damp (x : Float, y: Float, lambda: Float, dt: Float): Float
x
:以后点。y
:指标点。lambda
:较高的lambda
值能够使静止更加忽然,较低的值能够使静止更加平缓。dt
:以秒为单位的增量工夫。
实现
〇 资源引入
在文件顶部,咱们按上述原理中所述,引入必须的组件和办法。
import * as THREE from 'three'; | |
import {Suspense, useEffect} from 'react'; | |
import {Canvas, useFrame} from '@react-three/fiber'; | |
import {ScrollControls, Html, useScroll, useGLTF, useAnimations} from '@react-three/drei'; |
① 场景初始化
场景初始化非常简单,只需像上面这样增加 R3F
的 <Canvas />
组件,并初始化相机地位即可。
export default function Experience() { | |
return ( | |
<> | |
<Canvas camera={{position: [0, 0, 0] }}></Canvas> | |
</> | |
); | |
} |
② 加载模型
咱们先定义一个 ShubaDuck
类用来示意模型元素,而后应用 useGLTF
加载模型 🐸🦢
并应用模型文件的 scene
进行渲染。而后在 Canvas
中增加模型元素,并通过 scale
、position
等属性调整模型在页面上的显示大小和地位。在页面渲染前,咱们也能够应用 useGLTF.preload
对模型进行预加载,以进步页面应用体验。
function ShubaDuck({...props}) {const { scene, animations} = useGLTF('./models/duck.glb') | |
return <primitive object={scene} {...props} /> | |
} | |
useGLTF.preload('./models/duck.glb'); | |
<Canvas camera={{position: [0, 0, 0] }}> | |
<ShubaDuck scale={8} position={[0, -7, 0]} /> | |
</Canvas> |
以下为原始模型文件,下载完模型后能够咱们能够在 Blender
中查看模型构造和动画信息,删除批改不须要的元素,最初压缩导出。
页面中实现模型加载并渲染。
🔗
模型文件起源:https://sketchfab.com/3d-models/shuba-duck-54a6276ce06c4cc88fd497c8f1b8eb66
③ 播放模型骨骼动画
咱们从模型中拿到内置的骨骼动画 animations
,而后应用 useAnimations
获取到所有的动作,能够依据动作的名称进行动画播放比方本例中是 LironShuba
。能够在 useEffect
中对动作应用 play()
办法进行播放,本例中的 reset()
、fadeIn()
办法都是可选的,它们的作用别离是开始前重置动作和模型入场动画类型。
function ShubaDuck({...props}) {const { scene, animations} = useGLTF('./models/duck.glb') | |
const {actions} = useAnimations(animations, scene) | |
// 播放模型动画 | |
useEffect(() => void (actions['LironShuba'].reset().fadeIn(0.5).play()), [actions]) | |
// ... | |
} |
④ 滚动管制模型动画
当初咱们来增加 通过鼠标滚轮滚动来管制模型动画播放进度 的性能,即当咱们向下滚动页面时,模型动画正向播放,否则逆向播放,滚动速度越快,模型动画播放速度也越快。咱们先在 useEffect
中将模型动画初始状态设置为 pause
暂停,而后在 useFrame
页面重绘动画钩子函数中拿到动画动作和滚动百分比 scroll.offset
,设置动作播放工夫 action.time
,使其从初始值平滑过渡到目标值,目标值的确定能够通过整个动画播放周期时间以及页面滚动间隔的长度去计算,第三个参数 lambda
也能够依据本人的页面进行调整。
function ShubaDuck({...props}) { | |
// ... | |
useEffect(() => void (actions['LironShuba'].play().paused = true), [actions]) | |
useFrame((state, delta) => {const action = actions['LironShuba'] | |
const offset = scroll.offset | |
action.time = THREE.MathUtils.damp(action.time, (action.getClip().duration / 2) * offset, 100, delta) | |
state.camera.lookAt(0, 0, 0) | |
}) | |
// ... | |
} |
在页面中,咱们须要应用 <ScrollControls />
组将将模型组件 <ShubaDuck />
包裹起来。
<Canvas camera={{position: [0, 0, 0] }}> | |
<Suspense fallback={null}> | |
<ScrollControls pages={4}> | |
<ShubaDuck scale={10} position={[0, -10, 0]} /> | |
</ScrollControls> | |
</Suspense> | |
</Canvas> |
此时应用滚动管制模型动画性能就全副实现了,页面初始加载时模型是静止的,当咱们滚动页面时,模型依据滚动速度和滚动间隔就会自动播放对应的动画了。
💡
scroll.offset 是一个处于 [0, 1] 之间的数,示意滚动的百分比,当页面未滚动时值为 0,齐全滚动到止境时值为 1.
⑤ 滚动管制相机
在 useFrame
钩子函数中,咱们同样能够在页面滚动时动静批改相机 📷
的地位,这样在视觉上也能造成比方模型旋转、放大放大、显示暗藏等动画成果。本案例中应用了如下的设置,当页面从上往下滚动时,相机从模型侧面变换到模型侧面更远的中央,在视觉上造成模型转身并变小的成果 ✨
。
useFrame((state, delta) => { | |
// ... | |
state.camera.position.set(Math.sin(-offset) * 50, 1, Math.cos((offset * Math.PI) / 5) * 15) | |
}) |
⑥ 页面装璜
模型局部曾经全副实现了,此时咱们能够应用原理中介绍的 Html
组件,将其增加到页面中,并间接用 HTML
和 CSS
增加一些难看的页面,与模型主题响应。像上面这样,本文中增加了 5
个页面,每个页面都增加了不同的款式。有工夫的话,咱们也能够应用 gsap
等动画库,给 HTML
元素也增加一些滚动时的动画成果 ✨
。
<Canvas camera={{position: [0, 0, 0] }}> | |
<Suspense fallback={null}> | |
<ScrollControls pages={4}> | |
<Html wrapperClass='articles' occlude> | |
<article className='page page1'></article> | |
<article className='page page2'></article> | |
<article className='page page3'></article> | |
<article className='page page4'></article> | |
<article className='page page5'></article> | |
</Html> | |
<ShubaDuck scale={10} position={[0, -10, 0]} /> | |
</ScrollControls> | |
</Suspense> | |
</Canvas> |
第一页
第一页有较多的页面元素,其中底部红色文字应用了一种 woff2
格局的开源卡通字体,旋转的鹅 🦢
是通过如下 CSS
动画实现的。
@keyframes rotateY | |
from | |
transform: perspective(400px) rotateY(0deg) | |
to | |
transform: perspective(400px) rotateY(360deg) |
第二页
第二页是 3
个色块 🟩
通过旋转后造成的图案,给它们增加了明暗变动的动画成果。
其余页
剩下的页面应用了一些简略的文案或图片元素,等有工夫再优化小吧 😂
。其实还有很多细节款式和性能,如鼠标的款式是一个 🟡
、向下滚动的提醒语、顶部半透明的导航菜单等,具体实现可查看源码。
⑦ 点击菜单栏页面滑动动画
最初,咱们来给页面顶部的菜单增加一下点击操作 🖱
。点击页面顶部导航栏菜单滑动到对应页面性能实现应用了 element.scrollIntoView
办法,能够像上面这样实现并绑定到菜单的点击事件中,此时点击菜单页面滚动时,模型动画也会同时播放。
const handleMenuClick = (className) => {const page = document.querySelector(className); | |
page.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'nearest'}); | |
} | |
<span className='menu' onClick={handleMenuClick.bind(this, '.page1')}></span> |
🔗
源码地址:https://github.com/dragonir/threejs-odessey
总结
本文中次要蕴含的知识点包含:
R3F
生态中的ScrollControls
、Html
、useScroll
、useGLTF
、useAnimations
等组件和办法的根本用法。- 学会在
R3F
中加载模型并播放模型骨骼动画。 - 通过滚动管制模型动画播放过程和相机参数。
- 在滚动页面中将模型和
HTML
元素联合起来。 - 页面元素的一些
CSS
动画及页面整体丝滑滚动动画实现等。
想理解其余前端常识或其余未在本文中详细描述的 Web 3D 开发技术相干常识,可浏览我往期的文章。如果有疑难能够在评论中 留言 ,如果感觉文章对你有帮忙,不要忘了 一键三连哦 👍。
附录
- [1]. 🌴 Three.js 打造缤纷夏日 3D 梦中情岛
- [2]. 🔥 Three.js 实现炫酷的赛博朋克格调 3D 数字地球大屏
- [3]. 🐼 Three.js 实现 2022 冬奥主题 3D 趣味页面,含冰墩墩
- [4]. 🦊 Three.js 实现 3D 凋谢世界小游戏:阿狸的多元宇宙
- [5]. 🏡 Three.js 进阶之旅:全景漫游 - 高阶版在线看房
...
- 【Three.js 进阶之旅】系列专栏拜访 👈
- 更多往期【3D】专栏拜访 👈
- 更多往期【前端】专栏拜访 👈
参考
- [1]. threejs.org
- [2]. drei.pmnd.rs