关于three.js:Threejs-进阶之旅页面平滑滚动王国之泪-💧

申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。

摘要

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

成果

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

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

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

  • 👁‍🗨 在线预览地址:https://dragonir.github.io/tearsOfTheKingdom/

本专栏系列代码托管在 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)

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(...) })
  }, [])

@react-three/drei

@react-three/drei 是一个正在一直裁减的,用于 @react-three/fiber 的由实用的辅助工具、残缺的功能性办法以及现成的形象形成的库。能够通过如下办法进行装置。下图列出了以后该仓库中蕴含的所有组件和办法,本文案例中将通过 @react-three/drei 的以下几个组件来实现平滑滚动成果。

npm install @react-three/drei

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} />

实现

当初,咱们就利用上述原理常识,实现预览成果所示的 《塞尔达传说:王国之泪》 主题的平滑滚动页面。

资源引入

原理篇幅 👆 曾经具体解说了本文用到的性能库和组件,咱们在代码顶部像上面这样引入它们。

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>

💡 为了能够看见 canvas 画布区域,在 css 中设置了一个突变背景色。

页面装璜

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

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

首屏页面

首屏页面次要有 2 个元素,一个是背景图、另一个是 ZELDA 图片 logo,当页面加载时背景图有一个由大到小的缩放成果,鼠标悬浮到图片上时,以后鼠标所处图片会变为高亮状态。它们咱们能够应用原理局部理解到的 Image 元素实现。

图片组件封装

页面其余图片采纳的动画成果也是相似的,为了能够复用,咱们先对 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 图片仿佛有点问题,咱们能够像上面这样修复 😂

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

鼠标悬浮高亮成果:

页面平滑滚动:

其余页面

到这里,其余页面的实现就非常简单了,咱们只需按本人的页面设计,在 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 页,每页图片都有不同的排版款式。

完结页面

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

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

总结

本文中次要蕴含的知识点包含:

  • 理解 useFrame hook 基本原理及应用它管制渲染程序和应用负索引。
  • 理解 useThree hook 基本原理、根本属性值,应用它抉择属性、从组件循环内部读取状态、替换默认值等。
  • 理解 @react-three/drei 库的根本组成,学习应用它提供的 PreloaduseIntersectScrollControlsScroll、及 Image 等组件和办法。
  • CSS 生成简略的循环悬浮动画。
  • 应用上述 R3F 常识原理,生成一个具备视差成果的平滑滚动页面。

附录

  • [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]. React Three Filber
  • [3]. greensock 官网案例

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理