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日

相关文章

  • 不要小看注释掉的JS 引起的安全问题

    首先,注释掉的 JavaScript 代码是存在安全问题的,因为这些代码可以被黑客利用来进行攻击。因此,我们需要小心处理这些注释掉的代码。下面是一些攻略: 1. 审查代码,删除无用的注释信息 我们应该定期地审查我们的代码,删除无用的注释信息。在代码中注释掉的代码可能是过时的,已被修复或已不再需要。除此之外,注释信息还可能包含敏感信息,比如数据库密码、API …

    JavaScript 2023年6月11日
    00
  • JavaScript高级程序设计 读书笔记之八 Function类及闭包

    JavaScript高级程序设计 读书笔记之八 Function类及闭包 Function类 Function类的特点 Function类本身也是一个函数,它可以像工厂函数一样构建新的函数实例。 使用Function构造函数构建函数,可以动态地创建函数语句。 可以将字符串形式的代码,通过Function的形式执行。 Function构造函数的使用 Funct…

    JavaScript 2023年5月28日
    00
  • js 得到文件后缀(通过正则实现)

    要得到一个文件的后缀,可以通过以下步骤来实现: 步骤 1:获取完整文件名 首先,我们需要获取文件的完整文件名,可以通过以下方式来获取: let fileName = ‘example.txt’; 步骤 2:通过正则表达式获取文件后缀 我们可以使用正则表达式来获取文件的后缀,正则表达式的语法为: /\.[^.]+$/g 该正则表达式的含义为: /\. :匹配以…

    JavaScript 2023年5月27日
    00
  • 学习JavaScript设计模式(接口)

    学习JavaScript设计模式(接口)的完整攻略 什么是接口? 在JavaScript中,接口是一种抽象的概念,用于定义对象应该具有哪些方法。接口只定义方法名和参数,而没有具体的实现。 接口的作用 接口用于规范对象的行为,可以避免代码混乱和不可预测性。它定义了一种契约,约束了对象应该具有哪些方法。使用接口可以使代码更加灵活、可维护和可扩展。 如何定义接口?…

    JavaScript 2023年6月1日
    00
  • JS基于Location实现访问Url、重定向及刷新页面的方法分析

    让我详细讲解一下 “JS基于Location实现访问Url、重定向及刷新页面的方法分析” 的完整攻略。 什么是 Location 对象? Location 对象代表了当前窗口中当前文档的URL,还提供了与URL相关的信息和一些导航功能。它是window对象下的属性,所以可以通过 window.location 或者 location 来访问。 访问URL 获…

    JavaScript 2023年5月28日
    00
  • Web前端新人笔记之jquery入门心得(新手必看)

    来详细讲解一下“Web前端新人笔记之jquery入门心得(新手必看)”的完整攻略。 1. 前言 在介绍 jQuery 入门心得之前,需要先了解什么是 jQuery。jQuery 是一款快速、简洁的 JavaScript 库,是为了更简单地操作 HTML 文档、处理事件、实现动画效果和数据交互等功而开发的。jQuery 支持主流的浏览器,并且具有代码简洁、开发…

    JavaScript 2023年5月18日
    00
  • 很全面的JavaScript常用功能汇总集合

    欢迎来到网站,以下是“很全面的JavaScript常用功能汇总集合”的完整攻略。 什么是JavaScript常用功能汇总集合 JavaScript常用功能汇总集合是一个包含了常见的JavaScript功能和代码示例的集合,涵盖了诸如数组操作、字符串处理、函数式编程、事件处理等常用功能。通过本集合,您可以快速地获取JavaScript开发过程所需的绝大部分基础…

    JavaScript 2023年5月18日
    00
  • 动态加载JavaScript文件的3种方式

    当我们开发一个网站时,经常需要用到Javascript代码来处理交互逻辑和动态效果。通常,为了让代码更清晰、易于维护,我们会将Javascript代码分离到一个或多个独立的文件中。这时就需要用到动态加载Javascript文件的功能。下面介绍3种常用的方式: 1. 通过DOM创建script元素 动态加载Javascript文件最常用的方式就是通过DOM创建…

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