Three.js 进阶之旅:滚动控制模型动画和相机动画 🦢
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
摘要
专栏上篇文章《Three.js 进阶之旅:页面*滑滚动-王国之泪》 讲解并实现了如何使用 R3F
进行页面图片*滑滚动,本文内容在上节的基础上,学习如何使用滚动控制 ScrollControls
来控制模型的的动画播放和相机动画,通过滚动鼠标滚轮或者上下移动触摸板,来控制模型的动画播放进度或者相机的方位视角,从而呈现出惊艳的视觉效果。这种有趣的效果大家在*时浏览一些网页的时候应该经常见到,如一些 3D产品
介绍页向下滑动鼠标滚轮时产品同时旋转并根据产品的不同视角加载不同文案、或者 3D数字地球
根据滚轮的移动距离转到某个国家或地区、还有一些 个人简历
页面或时间轴页面也经常用到这种效果。通过本文的阅读和案例页面的实现,你将学习到的知识包括:R3F
生态中的 ScrollControls
、Html
、useScroll
、useGLTF
、useAnimations
等组件和方法的基本用法、在 R3F
中加载模型并播放模型骨骼动画、通过滚动控制模型动画播放进程和相机参数、页面元素的一些 CSS
动画及页面整体丝滑滚动动画实现等。
效果
本文案例的实现效果如下图所示,页面主体元素由一个三维模型 、及底部的
5
页 HTML
页面构成,页面初始加载时模型是静止的,当我们使用鼠标或触控板或直接拖动页面滚动条时 ,相机镜头
从正面*处*滑过渡到模型侧面远处,模型开始自动播放自带骨骼动画,模型动作根据页面滚动距离和滚动时速率的大小而不同。当我们点击页面顶部菜单时,页面会*滑滚动到对应位置,模型也会播放动画。
当页面逆向 滚动时,相机和模型动画也会逆向变化和播放。
文章使用 GIF
可能会造成丢帧或卡顿,可以亲自打开预览链接试试,大屏访问效果更佳。
本专栏系列代码托管在 Github
仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
原理
如果用原生 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>
总结
本文中主要包含的知识点包括:
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
本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17430114.html
Three.js 进阶之旅:滚动控制模型动画和相机动画 🦢的更多相关文章
- Three.js 进阶之旅:新春特典-Rabbit craft go 🐇
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 兔年到了,祝大家身体健,康万事顺利.本文内容作为兔年新春纪念页面,将使用 ...
- Three.js 进阶之旅:物理效果-碰撞和声音 💥
摘要 本文内容主要汇总如何在 Three.js 创建的 3D 世界中添加物理效果,使其更加真实.所谓物理效果指的是对象会有重力,它们可以相互碰撞,施加力之后可以移动,而且通过铰链和滑块还可以在移动过程 ...
- Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 本文在专栏上一篇内容<Three.js 进阶之旅:物理效果-碰撞和声 ...
- Three.js 进阶之旅:全景漫游-初阶移动相机版
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 3D 全景技术可以实现日常生活中的很多功能需求,比如地图的街景全景模式.数 ...
- Three.js 进阶之旅:全景漫游-高阶版在线看房 🏡
声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 专栏上篇文章<Three.js 进阶之旅:全景漫游-初阶移动相机版& ...
- 我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(三)Android客户端功能实现
我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(一)PC服务器端(地址:http://blog.csdn.net/ouyang_pen ...
- js进阶 12-6 监听鼠标滚动事件和窗口改变事件怎么写
js进阶 12-6 监听鼠标滚动事件和窗口改变事件怎么写 一.总结 一句话总结:滚动事件scroll(),浏览器窗口调整监听resize(),思考好监听对象. 1.滚动事件scroll()的监听对象是 ...
- 3D网页小实验-基于Babylon.js与recast.js实现RTS式单位控制
一.运行效果 1.建立一幅具有地形起伏和不同地貌纹理的地图: 地图中间为凹陷的河道,两角为突出的高地,高地和低地之间以斜坡通道相连. 水下为沙土材质,沙土材质网格贴合地形,河流材质网格则保持水平. 2 ...
- js 实现文字列表滚动效果
今天要实现抽奖名单在首页滚动展示的效果,就用js写了一个,代码如下: html代码: <style type="text/css"> *{margin:;padding ...
- js鼠标及对象坐标控制属性详细解析
对js鼠标及对象坐标控制属性进行了详细的分析介绍. offsetTop获取对象相对于版面或由 offsetParent 属性指定的父坐标的计算顶端位置. offsetLeft获取对象相对于版面或由 ...
随机推荐
- 深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析
深入理解 Python 虚拟机:复数(complex)的实现原理及源码剖析 在本篇文章当中主要给大家介绍在 cpython 虚拟机当中是如何实现 复数 complex 这个数据类型的,这个数据类型在 ...
- T-Dubbo,最好的RPC接口测试工具,支持nacos、zookeeper两大主流注册中心,真香!
这可能是有史以来最好用的RPC接口测试工具 文末有视频简介 获取方式 一只小Coder 简介 T-Dubbo,是一个基于Dubbo的全自动RPC接口测试平台为当下最流行的微服务架构中的RPC接口提供了 ...
- ZIP64压缩扩展的兼容性问题
一.ZIP压缩的两种规范 zip64 格式是标准 zip 格式的扩展,实际上消除了 zip 存档中文件大小和数量的限制. 每种格式允许的最大值总结如下: Standard Format Zip64 F ...
- 本地推理,单机运行,MacM1芯片系统基于大语言模型C++版本LLaMA部署“本地版”的ChatGPT
OpenAI公司基于GPT模型的ChatGPT风光无两,眼看它起朱楼,眼看它宴宾客,FaceBook终于坐不住了,发布了同样基于LLM的人工智能大语言模型LLaMA,号称包含70亿.130亿.330亿 ...
- python入门教程之十八正则表达式
re.match函数 re.match 尝试从字符串的起始位置匹配一个模式,如果不是起始位置匹配成功的话,match()就返回none. 函数语法: re.match(pattern, string, ...
- pysimplegui之tiee实例(附带github仓库地址)
今天想写一个文件管理器,结果整了一下午,还是自己看的源代码少,分析别人代码少,最终还是看别人代码才找到错误原因.#!/usr/bin/env python import sys import os i ...
- .Net Core工作流WorkFlowCore
前言 WorkFlowCore是一个针对.NetCore的轻量级的工作流引擎,提供了FluentAPI.多任务.持久化以及并行处理的功能,适合于小型工作流.责任链的需求开发.支持工作流长期运行,提供了 ...
- 活动预告 | Jax Diffusers 社区冲刺线上分享(还有北京线下活动)
我们的 Jax Diffuser 社区冲刺活动已经截止报名,全球有 200 多名参赛选手成功组成了约 70 支队伍共同参赛. 为了帮助参赛者更好的完成自己的项目,也为了与更多社区成员们分享扩散模型和生 ...
- Cesium加载ArcGIS Server4490且orgin -400 400的切片服务
Cesium在使用加载Cesium.ArcGisMapServerImageryProvider加载切片服务时,默认只支持wgs84的4326坐标系,不支持CGCS2000的4490坐标系. 如果是A ...
- 【LeetCode动态规划#13】买卖股票含冷冻期(状态众多,比较繁琐)、含手续费
最佳买卖股票时机含冷冻期 力扣题目链接(opens new window) 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 . 设计一个算法计算出最大利润.在满足以下约束条件下,你可以 ...