如何製作 Storybook Day 網頁上的 3D 效果?

来源:https://www.cnblogs.com/EnSnail/archive/2023/06/05/17457057.html
-Advertisement-
Play Games

Storybook 剛剛達到了一個重要的里程牌:7.0 版本!為了慶祝,該團隊舉辦了他們的第一次用戶大會 - [Storybook Day](https://storybook.js.org/day)。為了更特別,在活動頁面中添加了一個視覺上令人驚嘆的 3D 插圖。 原文:[How we built ...


Storybook 剛剛達到了一個重要的里程牌:7.0 版本!為了慶祝,該團隊舉辦了他們的第一次用戶大會 - Storybook Day。為了更特別,在活動頁面中添加了一個視覺上令人驚嘆的 3D 插圖。

Storybook Day

原文:How we built the Storybook Day 3D animation

源碼:storybook-day

3D 插圖使用 React Three Fiber (R3F) 實現,靈感來自俄羅斯方塊。在本文中,將深入探討。內容包含:

  • 避免物體與球體堆積重疊
  • 用擠壓法模擬俄羅斯方塊
  • 通過景深和陰影等增強視覺效果
  • 通過減少材料數量來優化性能

基本實現


腳手架創建:

npx create-react-app my-app --template typescript

安裝依賴:

npm i @react-three/fiber @react-three/drei canvas-sketch-util -S

App.tsx

import React from 'react';
import { Canvas } from '@react-three/fiber'
import BlocksScene from './BlocksScene'

function App() {
  return (
    <div style={{ height: '100vh' }}>
      <Canvas
        shadows
        gl={{ antialias: false, stencil: false }}
        camera={{ position: [0, 0, 30], near: 0.1, far: 60, fov: 45 }}
      >
        <color attach="background" args={['#e3f3ff']} />

        <ambientLight intensity={0.5} />
        <directionalLight castShadow position={[2.5, 12, 12]} intensity={1} />
        <pointLight position={[20, 20, 20]} intensity={1} />
        <pointLight position={[-20, -20, -20]} intensity={1} />

        <BlocksScene />
      </Canvas>
    </div>
  );
}

export default App;

BlocksScene.tsx

import React, { Suspense } from "react"
// @ts-ignore
import * as Random from 'canvas-sketch-util/random'
import Block, { blockTypes } from './Block'
import * as THREE from 'three'
import { Float } from '@react-three/drei'
import VersionText from './VersionText'

const size = 5.5
const colors = ['#FC521F', '#CA90FF', '#1EA7FD', '#FFAE00', '#37D5D3', '#FC521F', '#66BF3C']

const blocks = new Array(40).fill(0).map((_, index) => ({
  id: index,
  position: [Random.range(-size * 3, size * 3), Random.range(-size, size), Random.range(-size, size)],
  size: Random.range(0.1875, 0.375) * size,
  color: Random.pick(colors),
  type: Random.pick(blockTypes),
  rotation: new THREE.Quaternion(...Random.quaternion()),
}))
const BlocksScene = () => {
  return (
    <Suspense fallback={null}>
      <group position={[0, 0.5, 0]}>
        <VersionText />
        {blocks.map(block => (
          <Float
            key={block.id}
            position={block.position as any}
            quaternion={block.rotation}
            scale={block.size}
            speed={1}
            rotationIntensity={2}
            floatIntensity={2}
            floatingRange={[-0.25, 0.25]}
          >
            <Block type={block.type} color={block.color} />
          </Float>
        ))}
      </group>
    </Suspense>
  )
}

export default BlocksScene

Block.tsx

import React from "react"
import { Sphere, Cylinder, Torus, Cone, Box } from '@react-three/drei'

export const BLOCK_TYPES = {
  sphere: { shape: Sphere, args: [0.5, 32, 32] },
  cylinder: { shape: Cylinder, args: [0.5, 0.5, 1, 32] }, // 圓柱
  torus: { shape: Torus, args: [0.5, 0.25, 16, 32] }, // 圓環
  cone: { shape: Cone, args: [0.5, 1, 32] }, // 圓錐
  box: { shape: Box, args: [1, 1, 1] },
} as const
export type BlockType = keyof typeof BLOCK_TYPES
export const blockTypes = Object.keys(BLOCK_TYPES) as BlockType[]

interface BlockProps {
  type: BlockType
  color: string
}

const Block = ({ type, color }: BlockProps) => {
  const Component = BLOCK_TYPES[type].shape

  return (
    <Component args={BLOCK_TYPES[type].args as any} castShadow>
      <meshPhongMaterial color={color} />
    </Component>
  )
}

export default Block

VersionText.tsx

import React from 'react'
import { Center, Text3D } from '@react-three/drei'
import * as THREE from 'three'
import font from './font' // 字體比較多,參考:原文

const textProps = {
  font: font,
  curveSegments: 32,
  size: 10,
  height: 2.5,
  letterSpacing: -3.25,
  bevelEnabled: true,
  bevelSize: 0.04,
  bevelThickness: 0.1,
  bevelSegments: 3
}

const material = new THREE.MeshPhysicalMaterial({
  thickness: 20,
  roughness: 0.8,
  clearcoat: 0.9,
  clearcoatRoughness: 0.8,
  transmission: 0.9,
  ior: 1.25,
  envMapIntensity: 0,
  // color: '#0aff4f'
  color: '#9de1b4'
})

const VersionText = () => {
  return (
    <Center rotation={[-Math.PI * 0.03125, Math.PI * 0.0625, 0]}>
      {/* @ts-ignore */}
      <Text3D position={[-4, 0, 0]} {...textProps} material={material}>7.</Text3D>
      {/* @ts-ignore */}
      <Text3D position={[4, 0, 0]} {...textProps} material={material}>0</Text3D>
    </Center>
  )
}

export default VersionText

註意以上代碼,雖然讓塊隨機分佈在整個場景中了,但是有的與文本重疊或彼此重疊。如果這些塊沒有重疊,那在美學上會更令人愉悅。那麼如何避免重疊呢?

球體堆疊放置塊

pack-spheres 庫能夠讓塊均勻分佈,並防止任何潛在的重疊問題。該庫採用蠻力方法在立方體內排列不同半徑的球體。

安裝依賴

npm i pack-spheres -S
const spheres = pack({
  maxCount: 40,
  minRadius: 0.125,
  maxRadius: 0.25
})

縮放球體以適應場景空間,並沿 x 軸水平拉伸。最後,在每個球體的中心放置一個塊,縮放到球體的半徑。

這樣就實現了塊分佈,大小和位置也令人滿意。

處理文本和塊之間的重疊,需要一種不同的方法。最初,考慮使用 pack-spheres 來檢測球體和文本幾何體之間的碰撞。最終選擇了一個更簡單的解決方案:沿 z 軸稍微移動球體。

文本本質上是所有塊中的一部分。

全部更改都在 BlocksScene.tsx 文件中:

import React, { Suspense } from "react"
// @ts-ignore
import * as Random from 'canvas-sketch-util/random'
import Block, { blockTypes } from './Block'
import * as THREE from 'three'
import { Float } from '@react-three/drei'
import VersionText from './VersionText'
// @ts-ignore
import pack from 'pack-spheres'

const size = 5.5
const colors = ['#FC521F', '#CA90FF', '#1EA7FD', '#FFAE00', '#37D5D3', '#FC521F', '#66BF3C']

// 橫向拉伸
const scale = [size * 6, size, size]

const spheres = pack({
  maxCount: 40,
  minRadius: 0.125,
  maxRadius: 0.25
}).map((sphere: any) => {
  const inFront = sphere.position[2] >= 0

  return {
    ...sphere,
    position: [
      sphere.position[0],
      sphere.position[1],
      // 偏移以避免與 7.0 文本重疊
      inFront ? sphere.position[2] + 0.6 : sphere.position[2] - 0.6
    ]
  }
})

const blocks = spheres.map((sphere: any, index: number) => ({
  ...sphere,
  id: index,
  // 縮放 位置、半徑,適應場景
  position: sphere.position.map((v: number, idx: number) => v * scale[idx]),
  size: sphere.radius * size * 1.5,
  color: Random.pick(colors),
  type: Random.pick(blockTypes),
  rotation: new THREE.Quaternion(...Random.quaternion()),
}))

const BlocksScene = () => {
  return (
    <Suspense fallback={null}>
      <group position={[0, 0.5, 0]}>
        <VersionText />
        {blocks.map((block: any) => (
          <Float
            key={block.id}
            position={block.position as any}
            quaternion={block.rotation}
            scale={block.size}
            speed={1}
            rotationIntensity={2}
            floatIntensity={2}
            floatingRange={[-0.25, 0.25]}
          >
            <Block type={block.type} color={block.color} />
          </Float>
        ))}
      </group>
    </Suspense>
  )
}

export default BlocksScene

擠壓方式模擬俄羅斯方塊

到目前為止,只使用了基礎塊,還沒有俄羅斯風格的方塊。

Three.js 中的 ExtrudeGeometry 的概念非常有趣。可以使用類似於 SVG 路徑或 CSS 形狀的語法為其提供 2D 形狀,它將沿 z 軸拉伸它。次功能非常適合創建俄羅斯方塊。

擠出俄羅斯方塊

Drei 的 Extrude 提供了一種相對簡單的語法創建此類形狀。以下是如何生成 “T” 塊的示例:

import React, { useMemo } from 'react'
import * as THREE from 'three'
import { Extrude } from '@react-three/drei'

export const SIDE = 0.75
export const EXTRUDE_SETTINGS = {
  steps: 2,
  depth: SIDE * 0.75,
  bevelEnabled: false
}

export const TBlock = ({ color, ...props }: any) => {
  const shape = useMemo(() => {
    const _shape = new THREE.Shape()
    _shape.moveTo(0, 0)
    _shape.lineTo(SIDE, 0)
    _shape.lineTo(SIDE, SIDE * 3)
    _shape.lineTo(0, SIDE *3)
    _shape.lineTo(0, SIDE * 2)
    _shape.lineTo(-SIDE, SIDE * 2)
    _shape.lineTo(-SIDE, SIDE)
    _shape.lineTo(0, SIDE)
    
    return _shape
  }, [])

  return (
    <Extrude args={[shape, EXTRUDE_SETTINGS]} {...props}>
      <meshPhongMaterial color={color} />
    </Extrude>
  )
}

陰影

通過增加陰影深度可以使場景栩栩如生。可以在場景中設置光源和物體,使用 castShadow 投射陰影。為了提供更柔和的陰影,採用 Drei 提供的ContactShadows 組件。

ContactShadows 組件的陰影是一種“假陰影”效果。它們是通過從下方拍攝場景並將陰影渲染到接收器平面上來生成。陰影在幾幀中積累,更加柔和、逼真。

ContactShadows 組件可以通過調整解析度、不透明度、模糊、顏色等其他屬性來自定義外觀。

在 'App.tsx' 中加入 ContactShadows 組件,併進行設置。

import React from 'react';
import { Canvas } from '@react-three/fiber'
import { ContactShadows } from '@react-three/drei';
import BlocksScene from './BlocksScene'

function App() {

  return (
    <div style={{ height: '100vh' }}>
      <Canvas
        shadows
        gl={{ antialias: false, stencil: false }}
        camera={{ position: [0, 0, 30], near: 0.1, far: 60, fov: 45 }}
      >
        <color attach="background" args={['#e3f3ff']} />

        <ambientLight intensity={0.5} />
        <directionalLight castShadow position={[2.5, 12, 12]} intensity={1} />
        <pointLight position={[20, 20, 20]} intensity={1} />
        <pointLight position={[-20, -20, -20]} intensity={1} />

        <BlocksScene />

        <ContactShadows
          resolution={512}
          opacity={0.5}
          position={[0, -8, 0]}
          width={20}
          height={10}
          color='#333'
        />
      </Canvas>
    </div>
  );
}

export default App;

景深效果(深度模糊效果)

在此階段,場景中的每個對象都以相同的清晰度渲染,導致場景看起來有些平淡。攝影師會使用大光圈和淺景深來營造令人愉悅的模糊美感。可以通過對場景應用後處理(@react-three/postprocessing)來模擬這種效果,增加電影感。

EffectComposer 管理和運行後處理通道。它首先將場景渲染到緩衝區,然後在將最終圖像渲染到屏幕上之前應用一個濾鏡效果。

選取對焦距離

使用景深效果,可以將焦點放在場景中的特定距離(focusDistance)上,並使其他所有內容都變得模糊。但是如何定義對焦距離呢?它是以世界單位還是其他什麼方式衡量?

import { Canvas } from '@react-three/fiber';
import { EffectComposer, DepthOfField } from '@react-three/postprocessing';

export const Scene = () => (
  <Canvas>
    {/* Rest of Our scene */}
    <EffectComposer multisampling={8}>
      <DepthOfField focusDistance={0.5} bokehScale={7} focalLength={0.2} />
    </EffectComposer>
  </Canvas>
);

相機的視野由一個金字塔形狀的體積定義,稱為”視椎體“。距離相機最小(近平面)和最大(遠平面)距離內的物體將被渲染。

來自:3D 編程簡介 - 透視投影

focusDistance 參數表示處於焦點的物體距離相機的距離。它的值在 0 到 1 之間,其中 0 代表相機的近平面,1 代碼相機的遠平面。

本文將 focusDistance 設置為 0.5。靠近該值的物體將聚焦(清晰),而較遠的物體將模糊。將 bokehScale 設置為 7, 值為 0 時不模糊,值越大越模糊。

使用材料庫進行性能優化

陰影和景深是很酷的視覺效果,但它們的渲染成本相當高,會對性能產生重大影響。性能優化中,有用的建議是使用材料存儲來避免為每個塊創建新的材質實例。

Block 組件使用 color 為每個實例創建唯一的材質。例如,每個成色塊都有自己的材質實例。很浪費,對吧?

const Block = ({ type, color }: BlockProps) => {
  const Component = BLOCK_TYPES[type].shape

  return (
    <Component args={BLOCK_TYPES[type].args as any} castShadow>
      <meshPhongMaterial color={color} />
    </Component>
  )
}

通過使用材質存儲,可以在多個塊實例中重覆使用相同的材質。通過減少需要創建和渲染的材質數量提高性能。

import * as THREE from 'three';
THREE.ColorManagement.legacyMode = false;

const colors: string[] = [
  '#FC521F',
  '#CA90FF',
  '#1EA7FD',
  '#FFAE00',
  '#37D5D3',
  '#FC521F',
  '#66BF3C',
  '#0AB94F'
];

interface Materials {
  [color: string]: THREE.MeshPhongMaterial;
}

const materials: Materials = colors.reduce(
  (acc, color) => ({ ...acc, [color]: new THREE.MeshPhongMaterial({ color }) }),
  {}
);

export { colors, materials };

store 為每種可能的塊顏色生成一種材質,並將其存儲在對象中。塊組件無需為每個實例創建材質,只需從材質存儲中引用即可。

const Block = ({ type, color }: BlockProps) => {
  const Component = BLOCK_TYPES[type].shape;
  return (
    <Component
      args={OTHER_TYPES[type as OtherBlockType].args as any}
      material={materials[color]}
    />
  );
}

總結

3D 現在是 Web 的一部分, R3F 是將 HTML 和 WebGL 交織在一起的絕佳工具。R3F 生態系統非常豐富,drei 和 postprocessing 等庫簡化了複雜的 3D 任務。 Storybook Day 的 3D 場景完美地展示了平臺的可能性。使用球體包裝(pack-sphere)、擠壓(Extrude)、陰影、景深和材質存儲來創建令人難忘的活動頁面。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上期主要分享了 From Java To Kotlin 1 :空安全、擴展、函數、Lambda。 這是 From Java to Kotlin 第二期。 From Java to Kotlin 關鍵在於 **思維的轉變**。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 🛰🛰 我們在無論是在查閱別人的代碼,還是在實際項目開發的過程中,肯定都會使用導入導出的功能,有時候我們會搞混這幾種方式到底有什麼區別,今天我們就來細緻的區分一下: 導入導出方式⚔️⚔️ 我們都知道最常見的幾種導出方式無非是exp ...
  • 一、雙向綁定原理 Vue2採用的是觀察者-發佈訂閱模式,利用Object.defineProperty實現對數據已定義屬性的監控(定義觀察者模式), 編譯DOM時解析v-model等屬性以及對input框等註冊事件實現UI和JS的交互(也就是註冊發佈訂閱這模式); 詳細的是主要是定義一個Observ ...
  • 本文主要是介紹團隊在離線包技術方案上的探索,以及基於prefetch的離線包實現方案如何減少維護成本和開發成本。 ...
  • ## 1、使用場景 在日常開發中,我們會將重覆代碼抽象為一個函數或者組件,然後在需要時調用或者引入。但是,對於某些功能,這種方法可能不夠優雅或者不夠靈活。例如,我們可能需要在DOM元素上添加一些自定義屬性或者綁定一些事件,這些操作可能難以通過函數或組件來實現。這時,[自定義指令](https://v ...
  • 某次在Uniapp群看到有人問uniapp如何操作dom元素。 ![](https://img2023.cnblogs.com/blog/3112483/202306/3112483-20230605170528006-986335874.png) 他想對這張表標紅的區域,做dom元素獲取,因為產品 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1️⃣ set對象:數組快速去重 常規情況下,我們想要篩選唯一值,一般會想到遍曆數組然後逐個對比,或者使用成熟的庫比如lodash之類的。 不過,ES6帶來了一個新玩意兒!它引入了一個全新的對象類型:Set!而且,如果結合上...展開運算 ...
  • > 隨著人工智慧技術的不斷發展,阿裡體育等IT大廠,推出的“樂動力”、“天天跳繩”AI運動APP,讓**雲上運動會、線上運動會、健身打卡、AI體育指導**等概念空前火熱。那麼,能否將這些在APP成功應用的場景搬上小程式,分享這些概念的紅利呢?本系列文章就帶您一步一步從零開始開發一個AI運動小程式,本 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...