Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

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

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

摘要

浏览网页时,常被一些基于鼠标滚轮控制的页面动画所惊艳到,比如greensock 官网这些 showcase 案例页面就非常优秀,它们大多数都是使用 Tween.jsgaspgreensock 提供的一些动画扩展库实现的。使用 Three.js 也能很容易实现丝滑的滚动效果,本文使用 React + Three.js + React Three Fiber 技术栈,实现一个《塞尔达传说:王国之泪》主题风格基于滚动控制的平滑滚动图片展示页面。通过本文的阅读,你将学习到的知识点包括:了解 R3FuseFrame hookuseThree hook 基本原理及用法;了解 @react-three/drei 库的基本组成,学习使用它提供的 PreloaduseIntersectScrollControlsScroll、及 Image 等组件和方法;用 CSS 生成简单的循环悬浮动画等。

效果

本文案例的实现效果如下图所示,当页面每个模块滚动进入视区时,每个模块会具有平滑向上移动的视差效果,并且伴随着由大到小的缩放动画,当鼠标悬浮到当前模块时,模块会产生高亮 效果。除此之外,页面还有一些其他的装饰,比如塞尔达风格的页面背景和边框、具有缓动动画效果的希卡之石以及同样具有平滑滚动效果的文字装饰王国之泪四个字。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

页面的整体布局是这样的,总共有 7 页,即高度为 700vh,每一页都具有不同的布局风格样式,滚动时都会具有缓动效果。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

打开以下链接,在线预览效果,本文中的 gif 造成丢帧和画质损失,大屏访问效果更佳。

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

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

原理

本文是使用 React Three Fiber 实现的,它不仅可以非常容易实现漂亮的三维图形,在二维平面页面开发中也能大放异彩。在开始实现本文案例之前,我们先来汇总下本文中需要应用到的知识点。掌握这些原理和方法,可以帮助我们迅速构建一个交互体验极佳的平滑滚动页面。

useFrame

hook 允许在页面每一帧渲染的时候运行代码,比如更新渲染效果、控件等,与 Three.js 中调用 requestAnimationFrame 实行重绘动画效果是一样的。你将接收到状态值 state 和时钟增量 delta。回调函数将在渲染帧之前被调用,当组件卸载时,它会自动从渲染循环中注销。
·

useFrame((state, delta, xrFrame) => {
  // 此函数在共享渲染循环内以本机刷新率运行
});

? 注意,在 useFrame 中不能使用 setState 更新状态值!

控制渲染循序

如果你需要更多的控制,你可以传递一个数字渲染优先级值。这将导致 React Three Fiber 完全禁用自动渲染。现在,渲染顺序将由我们自己控制,这在后期渲染通道处理以及在多个视图渲染的场景下非常有用。

function Render() {
  // 控制渲染顺序
  useFrame(({ gl, scene, camera }) => {
    gl.render(scene, camera)
  }, 1)

function RenderOnTop() {
  // 这里将在上面 Render 方法的 useFrame 之后执行。
  useFrame(({ gl, ... }) => {
    gl.render(...)
  }, 2)

? 回调将按优先级值的升序(最低在前,最高在后)执行,类似于 DOM 的层级顺序。

负索引

使用负索引无法接管渲染循环控制,但如果确实必须对组件树中的 useFrame 序列进行排序,使用负索引将很有用。

function A() {
  // 此处将先执行
  useFrame(() => ..., -2)

function B() {
  // 此处将在A的 useFrame 之后执行
  useFrame(() => ..., -1)

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

useThree

hook 允许访问的状态模型包括默认渲染器 renderer 、场景 scene、相机 camera 等。它还提当前画布 canvas 在屏幕和视区中的坐标位置大小。它是动态自适应的,如果调整浏览器大小,将返回新的测量值,它适用于所有可能更改的状态对象。

import { useThree } from '@react-three/fiber'

function Foo() {
  const state = useThree()

State 属性值

属性 描述 类型
gl Renderer THREE.WebGLRenderer
scene Scene THREE.Scene
camera Camera THREE.PerspectiveCamera
raycaster 默认 raycaster THREE.Raycaster
pointer 包含更新的、规范化的、居中的指针坐标 THREE.Vector2
mouse 已被弃用,可以使用 pointer 代替处理坐标 THREE.Vector2
clock 正在运行的系统时钟 THREE.Clock
linear 当色彩空间为线性时为 true boolean
flat 未使用色调映射时为 true boolean
legacy 通过 THREE.ColorManagement 禁用全局色彩管理 boolean
frameloop 渲染模式: always, demand, never always, demand, never
performance 系统回归 { current: number, min: number, max: number, debounce: number, regress: () => void }
size Canvas 像素值尺寸 { width: number, height: number, top: number, left: number, updateStyle?: boolean }
viewport three.js 中视区的尺寸 { width: number, height: number, initialDpr: number, dpr: number, factor: number, distance: number, aspect: number, getCurrentViewport: (camera?: Camera, target?: THREE.Vector3, size?: Size) => Viewport }
xr XR 接口, 管理 WebXR 渲染 { connect: () => void, disconnect: () => void }
set 允许设置任何状态属性 (state: SetState<RootState>) => void
get 允许获取任意非响应式的状态属性 () => GetState<RootState>
invalidate 请求新的渲染, 相当于 frameloop === 'demand' () => void
advance 前进一个 tick, 相当于 frameloop === 'never' (timestamp: number, runGlobalEffects?: boolean) => void
setSize 调整画布大小 (width: number, height: number, updateStyle?: boolean, top?: number, left?: number) => void
setDpr 设置像素比 (dpr: number) => void
setFrameloop 设置当前渲染模式的快捷方式 (frameloop?: 'always', 'demand', 'never') => void
setEvents 设置事件图层的快捷方式 (events: Partial<EventManager<any>>) => void
onPointerMissed 对未命中目标的指针单击的响应 () => void
events 指针事件处理 { connected: TargetNode, handlers: Events, connect: (target: TargetNode) => void, disconnect: () => void }

选择属性

可以通过选择属性,避免对仅对关注的组件进行不必要的重新渲染,需要注意的是无法响应式地获得 Three.js 深层次的动态属性。

// 仅当默认相机发生变化时会触发重新渲染
const camera = useThree((state) => state.camera)
// 仅当尺寸发生变化时会触发
const viewport = useThree((state) => state.viewport)
// ❌ 不能响应式地获得three.js深层次地属性值变化
const zoom = useThree((state) => state.camera.zoom)

从组件循环外部读取状态

function Foo() {
  const get = useThree((state) => state.get)
  ...
  get() // 在任意位置获取最新状态

交换默认值

function Foo() {
  const set = useThree((state) => state.set)
  ...
  useEffect(() => {
    set({ camera: new THREE.OrthographicCamera(...) })
  }, [])

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

@react-three/drei

@react-three/drei 是一个正在不断扩充的,用于 @react-three/fiber 的由实用的辅助工具、完整的功能性方法以及现成的抽象构成的库。可以通过如下方法进行安装。下图列出了当前该仓库中包含的所有组件和方法,本文案例中将通过 @react-three/drei 的以下几个组件来实现平滑滚动效果。

npm install @react-three/drei

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

Preload

WebGLRenderer 只有材质被触发时才会进行编译,这可能会导致卡顿。此组件使用 gl.compile 预编译场景,确保应用从一开始就具有响应性。默认情况下,gl.compile 只会预加载可见对象,如果你提供了所有属性,它们可能会被忽略。

<Canvas>
  <Suspense fallback={null}>
    <Model />
    <Preload all />

useIntersect

它可以非常方便的检测三维元素是否可见,当对象进入视图或者处于视图之外时,可以通过它获得对象的可见性参考值,useIntersect 依赖于 THREE.Object3D.onBeforeRender 实现,因此它仅适用于有效渲染的对象,如 meshlinesprite等,在 groupobject3D 的骨骼中无法生效。

const ref = useIntersect((visible) => console.log('object is visible', visible))
return <mesh ref={ref} />

ScrollControls 和 Scroll

ScrollControls 可以在 canvas 前方创建一个 HTML 滚动容器,你放入滚动组件 <Scroll> 中的所有元素都将受到影响。你可以使用 useScroll 钩子对滚动事件进行监听并响应,它提供很多有用的数据,例如当前的滚动偏移量、增量以及用于范围查找的函数:rangecurvevisible。如果需要对滚动偏移做出响应,如对象进入或移除视图时添加淡入淡出效果等,则后面的方法非常有用。

ScrollControls 的可配置属性:

type ScrollControlsProps = {
  // 精度,默认值 0.00001
  eps?: number
  // 是否水平滚动,默认为 false,垂直滚动
  horizontal?: boolean
  // 是否开启无限滚动,默认为 false,该属性是实验性的
  infinite?: boolean
  // 定义滚动区域大小,每个 page 的高度是 100%,默认为 1
  pages?: number
  // 用于增加滚动间距的参数,默认为 1
  distance?: number
  // 滚动阻尼系数,以秒为单位,默认为 0.2
  damping?: number
  // 用于限制最大滚动速度,默认值为 Infinite
  maxSpeed?: number
  // 是否开启
  enabled?: boolean
  style?: React.CSSProperties
  children: React.ReactNode
}

可以像下面这样使用:

<ScrollControls pages={3} damping={0.1}>
  {/* 此处 Canvas 的内容不会滚动,但是可以接收 useScroll! */}
  <SomeModel />
  <Scroll>
    {/* 此处 Canvas 内容将产生滚动 */}
    <Foo position={[0, 0, 0]} />
    <Foo position={[0, viewport.height, 0]} />
    <Foo position={[0, viewport.height * 1, 0]} />
  </Scroll>
  <Scroll html>
    {/* 此处 DOM 内容将产生滚动 */}
    <h1>html in here (optional)</h1>
    <h1 style={{ top: '100vh' }}>second page</h1>
    <h1 style={{ top: '200vh' }}>third page</h1>
  </Scroll>
</ScrollControls>
function Foo(props) {
  const ref = useRef()
  // 通过 useScroll 钩子对滚动事件进行监听并响应
  const data = useScroll()
  useFrame(() => {
    // data.offset:当前滚动位置,介于 0 和 1 之间,受阻尼系数影响
    // data.delta:当前增量,介于 0 和 1 之间,受阻尼系数影响

    // 当滚动条处于起始位置时为 0,当达到滚动距离的 1/3 时,将增加到 1
    const a = data.range(0, 1 / 3)
    // 当达到滚动距离的 1/3 时将开始增加,当滚动到 2/3 时,将增加到 1
    const b = data.range(1 / 3, 1 / 3)
    // 与上述相同,但是两边的余量均为 0.1
    const c = data.range(1 / 3, 1 / 3, 0.1)
    // 将在所选范围的 0-1-0 之间移动
    const d = data.curve(1 / 3, 1 / 3)
    // 与上述相同,但是两边的余量均为 0.1
    const e = data.curve(1 / 3, 1 / 3, 0.1)
    // 如果偏移量在范围内,则返回 true,如果偏移量不在范围内,则返回 false。
    const f = data.visible(2 / 3, 1 / 3)
    // visible 方法同样可以接收一个余量参数
    const g = data.visible(2 / 3, 1 / 3, 0.1)
  })
  return <mesh ref={ref} {...props} />
}

Image

是一个自动开启平铺效果的基于着色器的图片组件,图片填充效果类似于 CSS 中的 background-size: cover;

function Foo() {
  const ref = useRef()
  useFrame(() => {
    ref.current.material.zoom = ...         // 1 或更大
    ref.current.material.grayscale = ...    // 介于 0 和 1 之间
    ref.current.material.color.set(...)     // 混合颜色
  })
  return <Image ref={ref} url="/file.jpg" />
}

给材质增加透明度:

<Image url="/file.jpg" transparent opacity={0.5} />

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

实现

现在,我们就应用上述原理知识,实现预览效果所示的 《塞尔达传说:王国之泪》 主题的平滑滚动页面。

资源引入

原理篇幅 ? 已经详细讲解了本文用到的功能库和组件,我们在代码顶部像下面这样引入它们。

import * as THREE from 'three'
import { Suspense, useRef, useState } from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { Preload, useIntersect, ScrollControls, Scroll, Image as ImageImpl } from '@react-three/drei'

场景初始化

专栏文章《Three.js 进阶之旅:物理效果-3D乒乓球小游戏》已经详细介绍过 React Three Fiber 入门知识。使用 R3F 初始化三维场景非常简单,像下面这样一行代码就能完成场景初始化。本次实现不需要精细的三维效果,因此渲染器的抗锯齿属性 antialias 可以设置为 false

<Canvas gl={{ antialias: false }} dpr={[1, 1.5]}></Canvas>

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

? 为了可以看见 canvas 画布区域,在 css 中设置了一个渐变背景色。

页面装饰

接着,使用 R3F 实现平滑的滚动效果之前,我们先来装饰一下页面,为了符合 《塞尔达传说:王国之类》 的主题,我在本页面中添加了游戏主题背景、边框、以及 希卡之石 动画。由于这些内容不是本文的重点,本文不再赘述,具体实现可以查看源码 ?

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

<>
  <Canvas gl={{ antialias: false }} dpr={[1, 1.5]}></Canvas>
  <div className='sheikah-box'></div>
</>

首屏页面

首屏页面主要有 2 个元素,一个是背景图、另一个是 ZELDA 图片 logo,当页面加载时背景图有一个由大到小的缩放效果,鼠标悬浮到图片上时,当前鼠标所处图片会变为高亮状态。它们我们可以使用原理部分了解到的 Image 元素实现。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

图片组件封装

页面其他图片采用的动画效果也是类似的,为了可以复用,我们先对 Image 元素封装一下,将由大到小的缩放效果以及鼠标悬浮的 hover 高亮效果添加到每个 Image 元素中:

function Image({ c = new THREE.Color(), ...props }) {
  const visible = useRef(false)
  const [hovered, hover] = useState(false)
  const ref = useIntersect((isVisible) => (visible.current = isVisible))
  useFrame((state, delta) => {
    // 鼠标悬浮时的图片材质颜色变化
    ref.current.material.color.lerp(c.set(hovered ? '#fff' : '#ccc'), hovered ? 0.4 : 0.05);
    // 图片滚动到视区时大小缩放变化
    ref.current.material.zoom = THREE.MathUtils.damp(ref.current.material.zoom, visible.current ? 1 : 4, 4, delta)
  })
  return <ImageImpl ref={ref} onPointerOver={() => hover(true)} onPointerOut={() => hover(false)} {...props} />
}

然后我们再封装一个名为 Imagesgroup 元素,用来统一管理页面上的所有图片,分别设置每个图片的链接、在页面上的位置、大小、透明度等一些个性化属性。

function Images() {
  const { width, height } = useThree((state) => state.viewport);
  const group = useRef();
  return (
    <group ref={group}>
      // 背景图片
      <Image position={[0, 0, 0]} scale={[width, height, 1]} url="/images/0.jpg" />
      // logo 图片
      <Image position={[0, 0, 1]} scale={3.2} url="/images/banner.png" transparent={true} />
    </group>
  )
}

图片组件使用

然后,使用 <ScrollControls><Scroll> 来直接生成滚动页面,在其中添加上述封装的 <Image> 组件,也可以在其中添加一些装饰性文字 王国之泪,同样可以进行滚动控制,使用 Preload 进行预加载,提升页面渲染性能。

<Canvas gl={{ antialias: false }} dpr={[1, 1.5]}>
  <Suspense fallback={null}>
    <ScrollControls damping={1} pages={7}>
      <Scroll>
        <Images />
      </Scroll>
      <Scroll html>
        <h1 className='text'>王</h1>
        <h1 className='text'>国</h1>
        <h1 className='text'>之</h1>
        <h1 className='text'>泪</h1>
      </Scroll>
    </ScrollControls>
    <Preload />
  </Suspense>
</Canvas>

此时我们可以看看实现效果,首先是图片元素进入视区时由大到小的缩放动画效果。中间的透明 logo 图片似乎有点问题,我们可以像下面这样修复 ?

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

function Images() {
  useFrame(() => {
    // 取消 zelda logo 缩放动画
    group.current.children[1].material.zoom = 1;
  });
  // ...
}

鼠标悬浮高亮效果:

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

页面平滑滚动:

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

其他页面

到这里,其他页面的实现就非常简单了,我们只需按自己的页面设计,在 Images 中像下面这样排好页面上所有需要平滑滚动的图片即可,可以通过 positionscale 等属性设置个性化调整图片在页面上的位置、大小、加载时机等,比如本文示例中第 2 页有 3 张图片、第 3 页有 1 张图片……

function Images() {
  return (
    <group ref={group}>
      {/* 第1页 */}
      <Image position={[0, 0, 0]} scale={[width, height, 1]} url="./images/0.jpg" />
      <Image position={[0, 0, 1]} scale={3.2} url="./images/banner.png" transparent={true} />
      {/* 第2页 */}
      <Image position={[-2.5, -height + 1, 2]} scale={3} url="./images/1.jpg" />
      <Image position={[0, -height, 3]} scale={2} url="./images/2.jpg" />
      <Image position={[1.25, -height - 1, 3.5]} scale={1.5} url="./images/3.jpg" />
      {/* 第3页 */}
      <Image position={[0, -height * 1.5, 2.5]} scale={[6, 3, 1]} url="./images/4.jpg" />
      {/* 第3页 */}
      <Image position={[0, -height * 2 - height / 4, 0]} scale={[width, height, 1]} url="./images/5.jpg" />
      {/* ... */}
    </group>
  )
}

下图是本文示例所有图片的页面布局,总共有 7 页,每页图片都有不同的排版样式。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

结束页面

最后一张页面,林克 由小变大平滑滚动进入视区,与背景形成视差效果,是通过调整它的 position.z 来实现这一效果的,大家在动手实践时可以尝试设置不同的值,以达到自己的预期效果。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

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

总结

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

  • 了解 useFrame hook 基本原理及使用它控制渲染顺序和使用负索引。
  • 了解 useThree hook 基本原理、基本属性值,使用它选择属性、从组件循环外部读取状态、交换默认值等。
  • 了解 @react-three/drei 库的基本组成,学习使用它提供的 PreloaduseIntersectScrollControlsScroll、及 Image 等组件和方法。
  • CSS 生成简单的循环悬浮动画。
  • 使用上述 R3F 知识原理,生成一个具有视差效果的平滑滚动页面。

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

附录

参考

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

Three.js 进阶之旅:页面平滑滚动-王国之泪 ?

原文链接:https://www.cnblogs.com/dragonir/p/17375879.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Three.js 进阶之旅:页面平滑滚动-王国之泪 ? - Python技术站

(0)
上一篇 2023年5月6日
下一篇 2023年5月7日

相关文章

  • JavaScript Window 打开新窗口(window.location.href、window.open、window.showModalDialog)

    JavaScript Window 打开新窗口 有3种常用的JavaScript方法可以打开一个新窗口,它们分别是window.location.href、window.open和window.showModalDialog。接下来,我们将详细讲解它们的使用方法和区别。 window.location.href window.location.href 可以…

    JavaScript 2023年6月11日
    00
  • js获取日期:昨天今天和明天、后天

    获取日期是 JavaScript 中比较常见的操作。为了方便地获取昨天、今天、明天、后天等日期信息,可以使用 JavaScript 的 Date 对象自带的方法来实现。 以下是获取昨天、今天、明天和后天日期的完整攻略。 获取当前日期 首先需要获取当前日期,可以使用 JavaScript 中的 Date 对象,使用 new Date() 方式来创建一个 Dat…

    JavaScript 2023年5月27日
    00
  • Bootstrap编写一个在当前网页弹出可关闭的对话框 非弹窗

    让我来给你详细讲解一下如何使用Bootstrap编写一个在当前网页弹出可关闭的对话框。以下是具体的步骤: 版本说明 在编写本文时,Bootstrap 的最新版本为 v5.1.0,所以以下过程中的代码也是基于该版本编写的。 准备工作 在使用 Bootstrap 之前,你需要在你的网页中先引入相关的 CSS 和 JavaScript 文件。本文以 CDN 引入为…

    JavaScript 2023年5月19日
    00
  • JavaScript的Backbone.js框架入门学习指引

    JavaScript的Backbone.js框架入门学习指引 什么是Backbone.js框架? Backbone.js是轻量级的JavaScript框架,用于在Web应用中提供MVC(Model-View-Controller)的结构。它被广泛应用于构建单页面应用程序(SPA)。 开始学习Backbone.js框架 为了开始学习Backbone.js框架,…

    JavaScript 2023年6月11日
    00
  • javascript实现unicode与ASCII相互转换的方法

    JavaScript实现Unicode与ASCII相互转换的方法 在JavaScript中,我们经常需要对字符进行编码和解码以便于传输和存储数据。Unicode和ASCII是两种常见的字符编码方式,其中Unicode支持更多的字符集。本文将介绍如何在JavaScript中实现Unicode和ASCII之间的相互转换。 Unicode转ASCII 将Unico…

    JavaScript 2023年5月19日
    00
  • JavaScript利用split函数按规定截取字符串(获取邮箱用户名)

    JavaScript利用split函数按规定截取字符串(获取邮箱用户名)的攻略如下: 1. split()函数的使用 JavaScript的split()函数可以把一个字符串分割成一个字符串数组,这个分隔符可以是一个字符,也可以是一个正则表达式。我们可以利用split()函数把邮箱地址按照“@”符号进行分割成两个部分,分别是邮箱用户名和邮箱域名。 let e…

    JavaScript 2023年5月28日
    00
  • JS实现DOM删除节点操作示例

    下面是JS实现DOM删除节点操作的完整攻略: 步骤一:获取要删除的节点 首先,我们需要获取要删除的节点。可以通过document.querySelector()或document.getElementById()等方法获取到要删除的节点。 示例1: // 通过id获取要删除的节点 var nodeToRemove = document.getElementB…

    JavaScript 2023年6月10日
    00
  • JavaScript Canvas实现兼容IE的兔子发射爆破动图特效

    JavaScript Canvas实现兼容IE的兔子发射爆破动图特效攻略如下: 1. 准备工作 在开始编写代码之前,我们需要准备开发环境和必要的素材文件。具体如下: 安装支持Canvas的浏览器,例如Chrome、Firefox等。 准备需要用到的图片素材,包括兔子、炮弹、爆炸等。建议使用PNG格式,并切图至透明背景。 创建一个HTML文件,并引入JavaS…

    JavaScript 2023年6月10日
    00
合作推广
合作推广
分享本页
返回顶部