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

摘要

专栏上篇文章《Three.js 进阶之旅:页面*滑滚动-王国之泪》 讲解并实现了如何使用 R3F 进行页面图片*滑滚动,本文内容在上节的基础上,学习如何使用滚动控制 ScrollControls 来控制模型的的动画播放和相机动画,通过滚动鼠标滚轮或者上下移动触摸板,来控制模型的动画播放进度或者相机的方位视角,从而呈现出惊艳的视觉效果。这种有趣的效果大家在*时浏览一些网页的时候应该经常见到,如一些 3D产品 介绍页向下滑动鼠标滚轮时产品同时旋转并根据产品的不同视角加载不同文案、或者 3D数字地球 根据滚轮的移动距离转到某个国家或地区、还有一些 个人简历 页面或时间轴页面也经常用到这种效果。通过本文的阅读和案例页面的实现,你将学习到的知识包括:R3F 生态中的 ScrollControlsHtmluseScrolluseGLTFuseAnimations 等组件和方法的基本用法、在 R3F 中加载模型并播放模型骨骼动画、通过滚动控制模型动画播放进程和相机参数、页面元素的一些 CSS 动画及页面整体丝滑滚动动画实现等。

效果

本文案例的实现效果如下图所示,页面主体元素由一个三维模型 、及底部的 5HTML 页面构成,页面初始加载时模型是静止的,当我们使用鼠标或触控板或直接拖动页面滚动条时 ,相机镜头 从正面*处*滑过渡到模型侧面远处,模型开始自动播放自带骨骼动画,模型动作根据页面滚动距离和滚动时速率的大小而不同。当我们点击页面顶部菜单时,页面会*滑滚动到对应位置,模型也会播放动画。

当页面逆向 滚动时,相机和模型动画也会逆向变化和播放。

文章使用 GIF 可能会造成丢帧或卡顿,可以亲自打开预览链接试试,大屏访问效果更佳。

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

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

原理

如果用原生 JavaScript 实现滚动动画效果,就需要监听滚动事件和计算滚动距离。本文还是和上篇文章《hree.js 进阶之旅:页面*滑滚动-王国之泪》一样,直接使用封装好的组件 ScrollControlsScroll 来实现,它们的详细用法和原理可前往上篇文章查看。本文中最终实现的页面需要加载模型并播放它自带的骨骼动画,因此用到了以下几个 @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

它是一个使用 useLoaderGLTFLoader 的方便钩子函数,它默认使用 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 以类似弹簧的方式从 xy *滑地插入一个数字,以保持与帧速率无关的运动。

.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 中添加模型元素,并通过 scaleposition 等属性调整模型在页面上的显示大小和位置。在页面渲染前,我们也可以使用 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 组件,将其添加到页面中,并直接用 HTMLCSS 添加一些好看的页面,与模型主题呼应。像下面这样,本文中添加了 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 生态中的 ScrollControlsHtmluseScrolluseGLTFuseAnimations 等组件和方法的基本用法。
  • 学会在 R3F 中加载模型并播放模型骨骼动画。
  • 通过滚动控制模型动画播放进程和相机参数。
  • 在滚动页面中将模型和 HTML 元素结合起来。
  • 页面元素的一些 CSS 动画及页面整体丝滑滚动动画实现等。

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

附录

参考

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17430114.html

Three.js 进阶之旅:滚动控制模型动画和相机动画 🦢的更多相关文章

  1. Three.js 进阶之旅:新春特典-Rabbit craft go 🐇

    声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 兔年到了,祝大家身体健,康万事顺利.本文内容作为兔年新春纪念页面,将使用 ...

  2. Three.js 进阶之旅:物理效果-碰撞和声音 💥

    摘要 本文内容主要汇总如何在 Three.js 创建的 3D 世界中添加物理效果,使其更加真实.所谓物理效果指的是对象会有重力,它们可以相互碰撞,施加力之后可以移动,而且通过铰链和滑块还可以在移动过程 ...

  3. Three.js 进阶之旅:物理效果-3D乒乓球小游戏 🏓

    声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 本文在专栏上一篇内容<Three.js 进阶之旅:物理效果-碰撞和声 ...

  4. Three.js 进阶之旅:全景漫游-初阶移动相机版

    声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 3D 全景技术可以实现日常生活中的很多功能需求,比如地图的街景全景模式.数 ...

  5. Three.js 进阶之旅:全景漫游-高阶版在线看房 🏡

    声明:本文涉及图文和模型素材仅用于个人学习.研究和欣赏,请勿二次修改.非法传播.转载.出版.商用.及进行其他获利行为. 摘要 专栏上篇文章<Three.js 进阶之旅:全景漫游-初阶移动相机版& ...

  6. 我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(三)Android客户端功能实现

    我的Android进阶之旅------>Android实现用Android手机控制PC端的关机和重启的功能(一)PC服务器端(地址:http://blog.csdn.net/ouyang_pen ...

  7. js进阶 12-6 监听鼠标滚动事件和窗口改变事件怎么写

    js进阶 12-6 监听鼠标滚动事件和窗口改变事件怎么写 一.总结 一句话总结:滚动事件scroll(),浏览器窗口调整监听resize(),思考好监听对象. 1.滚动事件scroll()的监听对象是 ...

  8. 3D网页小实验-基于Babylon.js与recast.js实现RTS式单位控制

    一.运行效果 1.建立一幅具有地形起伏和不同地貌纹理的地图: 地图中间为凹陷的河道,两角为突出的高地,高地和低地之间以斜坡通道相连. 水下为沙土材质,沙土材质网格贴合地形,河流材质网格则保持水平. 2 ...

  9. js 实现文字列表滚动效果

    今天要实现抽奖名单在首页滚动展示的效果,就用js写了一个,代码如下: html代码: <style type="text/css"> *{margin:;padding ...

  10. js鼠标及对象坐标控制属性详细解析

    对js鼠标及对象坐标控制属性进行了详细的分析介绍.  offsetTop获取对象相对于版面或由 offsetParent 属性指定的父坐标的计算顶端位置. offsetLeft获取对象相对于版面或由 ...

随机推荐

  1. CAS 6.x + Delegated Authentication SAML2.0 配置记录

    最近领导派了一个活儿, 需要把我们CAS系统的身份识别交给甲方的系统, 甲方的系统是SAML2.0的协议. 由于之前对SAML2.0协议了解不多,折腾了不少时间,在这里记录一下.以后忘掉还可以看看. ...

  2. 一站式微服务治理中台,Water v2.10.2 发布

    Water(水孕育万物...) Water 为项目开发.服务治理,提供一站式解决方案(可以理解为微服务架构支持套件).基于 Solon 框架开发,并支持完整的 Solon Cloud 规范:已在生产环 ...

  3. 什么是UV贴图和展开?

    转载:https://baijiahao.baidu.com/s?id=1673723122020029798&wfr=spider&for=pc UV贴图是用于轻松包装纹理的3D模型 ...

  4. 实现hypothesis在网页标注后同步到本地obsidian

    实现hypothesis在网页标注后同步到本地obsidian 遇到的question 2023.3.21日 在更改了自己的模板之后,可以能按照Todo的方式展现所有的标记,但是发现在同一个网页上增加 ...

  5. 重磅!Apache Hudi联合传智教育推出免费中文视频教程

    基础介绍 Apache Hudi(简称:Hudi)使得您能在hadoop兼容的存储之上存储大量数据,同时它还提供两种原语,使得除了经典的批处理之外,还可以在数据湖上进行流处理.这两种原语分别是: Up ...

  6. What's the best way to read and understand someone else's code?

    Find one thing you know the code does, and trace those actions backward, starting at the end Say, fo ...

  7. [Linux]常用命令之【cat/echo/iconv/vi/grep/find/head/tail】

    cat cat [选项] [文件].. # 一次显示整个文件或从键盘创建一个文件或将几个文件合并成一个文件 cat -n file1 # 编号文件内容再输出 echo -n 输出后不换行 -e 遇到转 ...

  8. 6.Get和Post

    1.概述 URL是一个资源描述符,一个URL用于描述一个网上资源 Get用于获取/查询资源信息,Post用于更新资源信息 2.联系和区别 2.1.Get后退刷新无害,Post需要重新提交: 2.2.G ...

  9. 五月二十五日jdbc基础知识点

    Jdbc连接数据库1.建立与数据库的连接1.1导入jdbc包1.2加载JDBC驱动java.lang.Class.forName(JDBCDriverClass);Class.forName(driv ...

  10. 三天吃透Redis八股文

    Redis连环40问,绝对够全! Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Re ...