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实现客户端兼容各浏览器创建csv并下载的方法

    在JavaScript中,可以通过Blob对象和URL.createObjectURL()方法来生成CSV文件,并通过a标签的download属性实现文件的下载。下面是实现客户端兼容各浏览器创建CSV并下载的完整攻略: 准备工作 在代码中创建用户需要下载的CSV内容数据 var data = [ [‘姓名’, ‘年龄’, ‘性别’], [‘张三’, ’18’…

    JavaScript 2023年5月27日
    00
  • Javascript入门学习第二篇 js类型第1/2页

    以下是“Javascript入门学习第二篇 js类型第1/2页”的完整攻略: Javascript类型 Javascript是一种弱类型语言,因此不需要在声明变量时指定变量的类型。Javascript中的类型可以分为以下几类: 原始类型(primitive types):包括数字(number)、字符串(string)、布尔值(boolean)、空(null…

    JavaScript 2023年6月10日
    00
  • JavaScript中检测数据类型的四种方法

    当我们在进行 JavaScript 的开发时,必须经常检测数据类型以确保代码的正确性。本文将介绍 JavaScript 中检测数据类型的四种方法。 方法一:typeof 操作符 typeof 操作符用于检测变量的数据类型,返回一个字符串,表明该变量的数据类型。 console.log(typeof ‘Hello World’); // string cons…

    JavaScript 2023年6月10日
    00
  • 在javaScript中检测数据类型的几种方式小结

    接下来我将详细讲解在JavaScript中检测数据类型的几种方式小结。 检测数据类型的几种方式 typeof typeof 操作符可以返回值的数据类型字符串。它只有一些简单的规则,可以处理大多数数据类型,但也存在一些特殊情况。如下所示: typeof 123; // "number" typeof "123"; // …

    JavaScript 2023年5月28日
    00
  • 解决JS请求服务器gbk文件乱码的问题

    当我们使用JavaScript从服务器请求文本数据时,可能会遇到文本编码不符合UTF-8(如GBK)的情况,导致在浏览器端显示出错了,我们需要对此进行处理。 1. 了解传统的字符编码方式 在 Web 开发初期,世界各地的计算机都有自己的字符编码规范,因此引起了字符编码混乱的情况。后来,Unicode 规范提出来,尝试解决全球字符集的问题,UTF-8 字符编码…

    JavaScript 2023年5月19日
    00
  • 今天是星期几的4种JS代码写法

    下面我将详细讲解今天是星期几的4种JS代码写法。 方法一:使用Date对象 很多人可能都知道JS中有一个Date对象,它可以获取当前时间和日期的信息。利用它计算今天是星期几非常容易。 const today = new Date().getDay(); const weekdays = ["Sunday", "Monday&qu…

    JavaScript 2023年5月27日
    00
  • 利用js正则表达式验证手机号,email地址,邮政编码

    对于利用js正则表达式验证手机号、email地址、邮政编码,可以按照以下步骤进行: 一、编写正则表达式 验证手机号 手机号码一般为11位数字组成,以1开头,比较简单,可以使用以下正则表达式进行匹配: /^1[3456789]\d{9}$/ 该表达式的解释: ^ 为开始符号,表示匹配输入的开始 1 为手机号码开头的数字,即必须以1开头 [3456789] 表示…

    JavaScript 2023年5月19日
    00
  • js正则表达式之exec方法讲解

    下面是关于“js正则表达式之exec方法讲解”的完整攻略。 exec方法介绍 正则表达式是一个非常重要的知识点,使用正则表达式可以进行文本匹配和替换,exec() 是Regexp对象的一个方法,用于在字符串中执行正则表达式的搜索,并返回包含结果的数组。如果没有找到匹配,它将返回 null。 该方法的语法如下所示: regexp.exec(str) 其中 re…

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