当前位置:   article > 正文

React Three Fiber快速入门

React Three Fiber快速入门

https://threejs-journey.com/lessons/what-are-react-and-react-three-fiber#学习笔记

1.基础知识

resize

填充模版
image.png

构建第一个场景

  • we didn’t have to create a scene
  • we didn’t have to create the webglrender
  • the scene is being rendered on each frame
  • the default settings are making it look appealing(antialias,encoding,etc.)
  • we didn’t have to place a perspectivecamera
  • we didn’t have to pull it back from the center

image.png

image.png

动画

useFrame

每帧都会执行的函数
React Three Fiber Documentation
示例:使用 useRef+useFrame 实现立方体动画:
image.png
但是直接这样写有一个问题,因为这是基于帧数的动画,每帧动一点,那么在不同帧率的电脑上动画的快慢将不一样
这时就可以使用 useFrame 自带的函数参数了来解决这个问题,第二个参数,通常称为 delta,为之上一帧到这一帧所花费的时间,基本上是一个定数,越高帧数的电脑的更新的要频繁,只越小,那么就可以:
image.png
而这个 state 则可以获取当前场景中的很多对象,如 webgl、camera 等很多全局对象,但有时候我们不需要每帧获取这些对象,只需要获取一次即可,那这时就可以使用另外一个 hook:useThree

useThree

这个 hook 可以为我们提供相同的 state,但仅仅在组件初始化时提供一次
image.png

自定义 extend

这里通过使用 extend 和 three 原生的 ortb 来构建一个鼠标控制器(先不使用官方自带的)
也很简单,通过查看 three.js 文档获取到 orbitControls 需要的参数
image.png
image.png

构建自定义几何体

image.png
但是直接在组件内构建创建顶点,如果有 state 数据,每当改变时代码将被重新执行,将重新创建顶点,这里就需要注意了:

useMomo

image.png
还可以使用 useEffect()来计算顶点法线
image.png

Canvas

camera

默认为透视相机:
image.png
相机环绕动画模板:
image.png

webgl 渲染器配置

image.png

色调映射

默认是采用了色调映射的,如果不需要可以取消掉
image.png
默认是采用的 ACES 色调映射

2.Drei

React Three Fiber 的核心特性就是复用,而其拥有很多复原的库、组件,有数百个 如:

  • camera controls
  • complex geometries
  • post-processing
  • html implementagion
  • loaders
  • environment settings
  • complex calculations
  • etc.

image.png

control

TransformControls

image.png
默认是平移变换,还可以改为旋转变换:
image.png
还可以改为 scale 缩放变换,默认为 translate
还有有一种使用方式是使用 ref 进行父级关联:
image.png
这种方式更好
还有一个 bug,就是相机会跟随移动:这时只需要加上这一句就可以了:
image.png

PivotContr

TransformControls 的一个替代方案,对于用户比较友好,而TransformControls对于开发者友好
pivotcontrols 不能像transformcontrols那样作为一个组工作如果我们希望它位于球体的中心,我们必须改变它的位置使用 anchor 属性
image.png
这个 anchor 为这个对象的相对位置
有很多个性化属性:如配置颜色等:
image.png

Html

单独放置:
image.png
放置到现有对象上面:
image.png
自定义 css,构建一个 class
image.png
然后再 css 文件中配置即可
image.png
image.png

3D text SDF

距离函数

Text

image.png
天机属性:
image.png

MeshReflectorMaterial

反射材质,只适用于平面网格几何
默认情况下:
image.png

image.png

3.Debug

leva

安装 npm i leva
image.png
每当这个 dubug 值被改变时,这个组件将 rerender,组件内的代码将重新执行
这就是它的工作原理
添加范围:
image.png
矢量:
image.png

color

image.png

other

image.png

button

import {button} from useCcontrol
image.png

choice

image.png

folders

image.png

注意事项

其还有一些属性,初始化属性
注意需要添加到 Canvas 外边
image.png

性能监控

npm install r3f-perf@6.5

import { Perf } from 'r3f-perf'
  • 1

添加到 Canvas 内部:

export default function Experience()
{
    // ...

    return <>

        <Perf />

        {/* ... */}

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

默认情况下,接口位于右上角,这与 Leva 冲突,但我们可以使用属性 position 进行更改:

<Perf position="top-left" />
  • 1

R3F-Perf 显示大量有用的信息。
我们甚至可以访问绘制调用的数量、内存使用情况、渲染场景所需的时间等。

const { perfVisible } = useControls({
    perfVisible: true
})
  • 1
  • 2
  • 3
{ perfVisible && <Perf position="top-left" /> }
  • 1

4.Environment and Staging with R3F

[1]Background color

如果想要的只是均匀的颜色,那么这些技术中的任何一种都是可行的解决方案

With CSS
html,
body,
#root
{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: red;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

image.png

在渲染器上使用 setClearColor

WebGLRenderer 有一个名为 setClearColor .这是一种在渲染场景中的各种对象之前 用颜色填充的方法。
要使用 setClearColor ,我们需要访问渲染器,并且只需要在创建渲染器时执行一次。
在 index.js 创建一个 created 函数并将其发送到 名为 onCreated :

const created = () =>
{
    console.log('created')
}

root.render(
    <Canvas
        camera={ {
            fov: 45,
            near: 0.1,
            far: 200,
            position: [ - 4, 3, 6 ]
        } }
        onCreated={ created }
    >
        <Experience />
    </Canvas>
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
const created = ({ gl }) =>
{
    gl.setClearColor('#ff0000', 1)
}
  • 1
  • 2
  • 3
  • 4

同样效果

With the scene background
import * as THREE from 'three'

// ...

const created = ({ scene }) =>
{
    scene.background = new THREE.Color('#ff0000')
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
With R3F color
import './style.css'
import ReactDOM from 'react-dom/client'
import { Canvas } from '@react-three/fiber'
import Experience from './Experience.js'

const root = ReactDOM.createRoot(document.querySelector('#root'))

root.render(
  <Canvas
    camera={ {
        fov: 45,
        near: 0.1,
        far: 50,
        position: [ 1, 2, 6 ]
    } }
>
     <color args={ [ '#ff0000' ] } attach="background" />
    <Experience />
</Canvas>
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

[2]Lights

R3F 支持所有默认Three.js灯:

  • <ambientLight />
  • <hemisphereLight />
  • <directionalLight />
  • <pointLight />
  • <rectAreaLight />
  • <spotLight />
Light Helpers
export default function Experience()
{
    const directionalLight = useRef()

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
<directionalLight ref={ directionalLight } position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
  • 1
import { useHelper, OrbitControls } from '@react-three/drei'
  • 1

第一个参数是对光源的 useHelper 引用,第二个参数是我们想要从Three.js使用的帮助程序类。
这意味着我们首先需要导入 THREE 才能访问该 DirectionalLightHelper 类:

import * as THREE from 'three'
  • 1
export default function Experience()
{
    const directionalLight = useRef()
    useHelper(directionalLight, THREE.DirectionalLightHelper, 1)

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

image.png
useHelper 不仅仅是为了光线,因为我们可以将其用于相机 CameraHelper 作为示例。

[3]Shadows

我们将从默认的 Three.js 阴影系统开始,但由于 R3F 和 drei,我们将看到其他阴影解决方案变得更加容易。

Activation

要开启WebGLRenderer的阴影渲染 ,我们需要做的就是在 中 index.js 添加一个 shadows 属性:

root.render(
    <Canvas
        shadows
        camera={ {
            fov: 45,
            near: 0.1,
            far: 50,
            position: [ - 4, 3, 6 ]
        } }
    >
        <Experience />
    </Canvas>
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

回到 Experience.js ,我们添加 castShadow<directionalLight>

<directionalLight ref={ directionalLight } castShadow position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
  • 1

添加 castShadow 球体 和立方体 (这些对象只需要投射阴影,因为上面什么都没有):

<mesh castShadow position-x={ - 2 }>
    {/* ... */}
</mesh>

<mesh castShadow position-x={ 2 } scale={ 1.5 }>
    {/* ... */}
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后,在地板上 添加 receiveShadow (地板只需要接收阴影,因为下面什么都没有):

<mesh receiveShadow position-y={ - 1 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
    {/* ... */}
</mesh>
  • 1
  • 2
  • 3

image.png

Baking

如果我们的场景是静态的(事实并非如此,因为立方体在旋转,但我们无论如何都会这样做),我们可以添加来自 drei 的 BakeShadows 帮助程序。
这将只渲染一次阴影,而不是在每一帧上渲染。
导入 BakeShadows 自 @react-three/drei :

import { BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
  • 1
export default function Experience()
{
    // ...

    return <>

        <BakeShadows />
        {/* ... */}

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

立方体阴影中看到的那样,阴影不会在每一帧上更新,从而提高了性能

配置阴影

默认情况下,阴影贴图分辨率相当低,以保持稳定的性能。
在纯 JavaScript 中,我们可以通过 做 directionalLight.shadow.mapSize.set(1024, 1024) 来访问它,但是我们如何在 R3F 中做到这一点呢?
好消息是,大多数属性(甚至是深层属性)仍然可以通过用破折号分隔不同的深度级别来直接从属性中访问 - 。
例如,要更改 shadow.mapSize 属性,我们可以使用属性 shadow-mapSize (您可能需要重新加载):

<directionalLight
    ref={ directionalLight }
    position={ [ 1, 2, 3 ] }
    intensity={ 1.5 }
    castShadow
    shadow-mapSize={ [ 1024, 1024 ] }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

image.png
我们可以对 near 、 far 、 topbottom 和 leftright 属性执行相同的操作(因为 a OrthographicCamera 用于渲染阴影贴图):

<directionalLight
    ref={ directionalLight }
    position={ [ 1, 2, 3 ] }
    intensity={ 1.5 }
    castShadow
    shadow-mapSize={ [ 1024, 1024 ] }
    shadow-camera-near={ 1 }
    shadow-camera-far={ 10 }
    shadow-camera-top={ 2 }
    shadow-camera-right={ 2 }
    shadow-camera-bottom={ - 2 }
    shadow-camera-left={ - 2 }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

image.png
阴影被剪切是因为值太小,但这里的主要目的是解释如何调整这些阴影。
调整下:

<directionalLight
    ref={ directionalLight }
    position={ [ 1, 2, 3 ] }
    intensity={ 1.5 }
    castShadow
    shadow-mapSize={ [ 1024, 1024 ] }
    shadow-camera-near={ 1 }
    shadow-camera-far={ 10 }
    shadow-camera-top={ 5 }
    shadow-camera-right={ 5 }
    shadow-camera-bottom={ - 5 }
    shadow-camera-left={ - 5 }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

image.png

[4]Soft shadows

默认阴影太清晰。有多种方法可以软化它们,我们将发现一种称为百分比更近柔和阴影 (PCSS) 的技术。
这个想法是根据投射阴影的表面和接收阴影的表面之间的距离,通过在偏移位置选择阴影贴图纹理来使阴影看起来模糊,这在现实生活中就是这样发生的
It is achieved in Three.js thanks to spidersharma03 and we can find an example here https://threejs.org/examples/#webgl_shadowmap_pcss
threejs 采用的是修改着色器模块来实现的,而这里有softShadows(),这个函数会直接修改Three.js着色器

import { softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'

// ...

softShadows({
    frustum: 3.75,
    size: 0.005,
    near: 9.5,
    samples: 17,
    rings: 11
})

export default function Experience()
{
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

image.png
softShadows() 参数直接用于编译着色器中,修改它们意味着重新编译所有材质着色器,这就是为什么我们不能实时更改它或将它们添加到调试 UI 中的原因

[5]Accumulative Shadows

顾名思义,将 AccumulativeShadows 累积多个阴影渲染,我们将在每次渲染之前随机移动光线。这样,阴影将由一堆不同角度的渲染组成,使其看起来柔和且非常逼真。
AccumulativeShadows 将在平面上渲染。它限制了它的使用,但它在场景的地板上看起来非常好。
地板取消receiveShadow

<mesh position-y={ - 1 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
  {/* ... */}
</mesh>
  • 1
  • 2
  • 3
import { AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
  • 1
<AccumulativeShadows></AccumulativeShadows>
  • 1

需要配置属性

<AccumulativeShadows
  position={ [ 0, - 0.99, 0 ] }
  scale={ 10 }
  >
  <directionalLight
    position={ [ 1, 2, 3 ] }
    castShadow
    />
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image.png
<RandomizedLight> 具有多个属性来控制灯光的行为:

  • amount :多少盏灯(默认有多盏灯)
  • radius :抖动的幅度
  • intensity :灯光的强度
  • ambient :就像全局光照亮整个场景一样,只使狭窄的空间和缝隙接收阴影

以及与阴影贴图相关的参数:

  • castShadow :如果它应该投射阴影
  • bias :偏置偏移,用于修复对象在自身上投射阴影或未在非常靠近其表面的对象上投射阴影的问题
  • mapSize :阴影贴图大小(越低,性能越好)
  • size :阴影的振幅 ( top , right , bottom and left all at once)
  • near 和 far :阴影贴图相机渲染对象的距离和距离

找到最好的调整很复杂,这就是像 Leva 这样的调试 UI 会非常有帮助的地方。
但是让我们偷懒一点,把以下参数放进去:

<RandomizedLight
    amount={ 8 }
    radius={ 1 }
    ambient={ 0.5 }
    intensity={ 1 }
    position={ [ 1, 2, 3 ] }
    bias={ 0.001 }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

image.png
回到我们还可以访问一些属性 的地方:

  • colors :阴影的颜色
  • opacity :阴影的不透明度

由于我们有一个绿色的地板,让我们将阴影设置为深绿色/蓝色, color 并略微减少 opacity :

<AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
>
    {/* ... */}
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

image.png

  • frames :要执行多少个阴影渲染
  • temporal :将渲染分布在多个帧上
<AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
    frames={ 1000 }
>
    {/* ... */}
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image.png
1000 很多,但正如你所看到的,阴影看起来非常平滑。问题在于,Three.js必须在第一帧上一次性完成这些 1000 渲染,您可能会注意到相当长的冻结时间。
这不一定是什么大不了的,反正 1000 太多了,但我们可以通过以下方式 temporal 防止冻结:

<AccumulativeShadows
  position={ [ 0, - 0.99, 0 ] }
  scale={ 10 }
  color="#316d39"
  opacity={ 0.8 }
  frames={ 1000 }
  temporal
  >
  {/* ... */}
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

正如你所看到的,我们需要很多秒才能恢复我们的阴影,但至少我们没有任何冻结,因为每一帧只做了一个渲染。
您可能已经注意到的另一件事是,如果您移动相机,阴影上会绘制一个奇怪的形状。这是由于定向光助手弄乱了阴影贴图。
您可能已经注意到,阴影似乎没有根据立方体移动,这是真的。
为了更清楚地看到这一点,让我们让立方体左右移动。
在 useFrame 中,检索我们在上一课中所做的操作, clockelapsedTime 并将其分配给一个 time 变量:

useFrame((state, delta) =>
{
    const time = state.clock.elapsedTime
    // ...
})
  • 1
  • 2
  • 3
  • 4
  • 5

现在在立方体位置上使用它, Math.sin() 并添加 2 它,使其保持在当前位置附近:

useFrame((state, delta) =>
{
		const time = state.clock.elapsedTime
		cube.current.position.x = 2 + Math.sin(time)
		// ...
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

阴影似乎移动了一会儿(准确地说是 100 帧),然后停止了。这是因为我们特别要求只 AccumulativeShadows 渲染 100 帧。
解决方案是告诉 AccumulativeShadows 继续渲染阴影,我们可以通过将 frames 属性设置为 Infinity :

<AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
    frames={ Infinity }
    temporal
>
    {/* ... */}
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

不错,但有点跳跃。我们可以区分淡入和淡出的不同阴影贴图。原因是,当使用 infinite frames 时,只 AccumulativeShadows 混合最后 20 个阴影渲染。我们可以使用 blend 属性更改此值。
将 blend 属性设置为 100 :

<AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
    frames={ Infinity }
    temporal
    blend={ 100 }
>
    {/* ... */}
</AccumulativeShadows>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

好多了,但越 blend 高,在快速移动的物体上看到阴影的机会就越小。
由于 是 AccumulativeShadow 高度可参数化的,您应该将各种参数添加到像 Leva 这样的调试 UI 中,以便找到完美的调整。
这就是 AccumulativeShadow .注释或删除我们添加到立方体中的位置动画,并放回光助手:

export default function Experience()
{
    // ...
    useHelper(directionalLight, THREE.DirectionalLightHelper, 1)
    
    useFrame((state, delta) =>
    {
        // const time = state.clock.elapsedTime
        // cube.current.position.x = 2 + Math.sin(time)
        cube.current.rotation.y += delta * 0.2
    })
    
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

[6]Contact Shadows

最后一个影子解决方案称为 ContactShadows
因为它不依赖于 Three.js 的默认影子系统,所以我们将在 上停用 shadows (您也可以删除该属性):

<Canvas
    shadows={ false }
    camera={ {
        fov: 45,
        near: 0.1,
        far: 50,
        position: [ - 4, 3, 6 ]
    } }
>
    <Experience />
</Canvas>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

并评论或删除 :

{/* <AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
    frames={ Infinity }
    temporal
    blend={ 100 }
>
    <RandomizedLight
        amount={ 8 }
        radius={ 1 }
        ambient={ 0.5 }
        intensity={ 1 }
        position={ [ 1, 2, 3 ] }
        bias={ 0.001 }
    />
</AccumulativeShadows> */}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

image.png
首先要了解 ContactShadows 的是,它无需光即可工作。很奇怪,对吧?
就像 一样 AccumulativeShadows ,它可以在限制其使用的平面上工作,但在地板上看起来非常好。
ContactShadows 这将使整个场景有点像定向光,但相机取代了地板而不是光线。
然后,它会模糊阴影贴图,使其看起来更好。

import { ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
  • 1
export default function Experience()
{
    // ...

    return <>
        
        {/* ... */}
        
        <ContactShadows />

        {/* ... */}

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

image.png
默认配置并不好用

<ContactShadows
    position={ [ 0, - 0.99, 0 ] }
/>
  • 1
  • 2
  • 3

image.png

使用 debug 调试
import { useControls } from 'leva'
  • 1
export default function Experience()
{
    // ...

    const { color, opacity, blur } = useControls('contact shadows', {
        color: '#000000',
        opacity: { value: 0.5, min: 0, max: 1 },
        blur: { value: 1, min: 0, max: 10 },
    })

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image.png

<ContactShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    resolution={ 512 }
    far={ 5 }
    color={ color }
    opacity={ opacity }
    blur={ blur }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以下参数效果很好:

const { color, opacity, blur } = useControls('contact shadows', {
    color: '#1d8f75',
    opacity: { value: 0.4, min: 0, max: 1 },
    blur: { value: 2.8, min: 0, max: 10 },
})
  • 1
  • 2
  • 3
  • 4
  • 5

image.png
正如我们之前提到的,它将 ContactShadow 渲染从下方看到的场景,并使用该信息来生成阴影。然后,这个阴影将被模糊。对于小场景,性能应该很好,但对于更复杂的场景,此过程可能太重并导致帧速率下降,尤其是因为它必须在每一帧上完成
幸运的是,有一种方法可以通过将 frames 属性设置为 来 1 烘烤阴影:

<ContactShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    resolution={ 512 }
    far={ 5 }
    color={ color }
    opacity={ opacity }
    blur={ blur }
    frames={ 1 }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们选择的数字对应于将生成阴影的帧数。由于我们只想生成一次,因此我们选择了 1 .

局限性
  • The shadows always comes from the front of the plane (positive y in our case).
  • 这在物理上并不准确
  • 无论与物体的距离如何,它都会模糊阴影
  • 它对性能有相当大的影响

对于简单的对象显示,这很好,但对于更复杂或更逼真的渲染,您最好使用其他阴影解决方案。

[7]Sky

为了使场景更加逼真,并为了添加漂亮的自然背部,我们可以使用天空类 https://threejs.org/examples/webgl_shaders_sky.html
与往常一样,R3F 和 drei 通过 Sky 助手使任务变得非常容易。

import { Sky, ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
  • 1
export default function Experience()
{
    // ...

    return <>
        
        {/* ... */}
        
        <Sky />

        {/* ... */}

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

配置调整太阳的位置:

const { sunPosition } = useControls('sky', {
    sunPosition: { value: [ 1, 2, 3 ] }
})
  • 1
  • 2
  • 3
<Sky sunPosition={ sunPosition } />
  • 1

最后,为了使场景更加逼真和合乎逻辑,我们可以将 sunPosition 用于 :

<directionalLight
    ref={ directionalLight }
    position={ sunPosition }
    intensity={ 1.5 }
    castShadow
    shadow-mapSize={ [ 1024, 1024 ] }
    shadow-camera-near={ 1 }
    shadow-camera-far={ 10 }
    shadow-camera-top={ 5 }
    shadow-camera-right={ 5 }
    shadow-camera-bottom={ - 5 }
    shadow-camera-left={ - 5 }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

[8]Environment Map

之前可以使用六张图片和 360 图片做环境贴图,现在 drei 有现成的了

使用立方体纹理进行设置
import { Environment, useHelper, OrbitControls } from '@react-three/drei'
  • 1

首先,我们将使用您可以在 /public/environmentMaps/ 文件夹中找到的传统立方体纹理。
<Environment> 将 添加到 JSX 中,并将其 files 属性设置为包含纹理数组:

export default function Experience()
{
    // ...

    return <>
        
        <Environment
            files={ [
                './environmentMaps/2/px.jpg',
                './environmentMaps/2/nx.jpg',
                './environmentMaps/2/py.jpg',
                './environmentMaps/2/ny.jpg',
                './environmentMaps/2/pz.jpg',
                './environmentMaps/2/nz.jpg',
            ] }
        />

        {/* ... */}
    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
Intensity

默认值 envMapIntensity 设置为 1
调用 useControls ,将第一个参数设置为 以便 'environment map' 拥有具有该名称的文件夹,并发送一个 envMapIntensity 属性范围介于 和 12 之间的 0 对象(不要忘记检索 envMapIntensity ):

const { envMapIntensity } = useControls('environment map', {
    envMapIntensity: { value: 1, min: 0, max: 12 }
})
  • 1
  • 2
  • 3
<mesh castShadow position-x={ - 2 }>
    <sphereGeometry />
    <meshStandardMaterial color="orange" envMapIntensity={ envMapIntensity } />
</mesh>

<mesh castShadow ref={ cube } position-x={ 2 } scale={ 1.5 }>
    <boxGeometry />
    <meshStandardMaterial color="mediumpurple" envMapIntensity={ envMapIntensity } />
</mesh>

<mesh receiveShadow position-y={ - 1 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
    <planeGeometry />
    <meshStandardMaterial color="greenyellow" envMapIntensity={ envMapIntensity } />
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
Background skybox

上面的只设置了环境贴图,想要设置全景图还需要加上 background 属性

<Environment
    background
    files={ [
        './environmentMaps/2/px.jpg',
        './environmentMaps/2/nx.jpg',
        './environmentMaps/2/py.jpg',
        './environmentMaps/2/ny.jpg',
        './environmentMaps/2/pz.jpg',
        './environmentMaps/2/nz.jpg',
    ] }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

image.png

HDRI texture

我们可以使用一张覆盖周围环境的图像,而不是使用 6 张图像。
它就像一张 360 度全景照片,通常处于高动态范围,以使照明数据更准确。这是有道理的,因为光不会真正停留在某个范围内。如果你看太阳,它会比看灯泡灯亮得多(不要看太阳)。
the_sky_is_on_fire_2k.hdr

<Environment
    background
    files="./environmentMaps/the_sky_is_on_fire_2k.hdr"
/>
  • 1
  • 2
  • 3
  • 4

image.png
HDRIS 下载网址:
https://polyhaven.com/hdris

Presets 预设

比下载这些 HDRI 更好的是,drei 创建了直接从 Poly Haven 获取文件的预设。
替换 files 为 preset

<Environment
    background
    preset="sunset"
/>
  • 1
  • 2
  • 3
  • 4

https://github.com/pmndrs/drei/blob/master/src/helpers/environment-assets.ts
image.png

[9]Custom environment

假设我们希望在一侧有某种大的红色矩形,以确保有红光从这一侧照亮我们的物体。
All we have to do is position a (or anything we want to be part of the environment map) inside the

<Environment
    background
    preset="sunset"
>
    <mesh position-z={ - 5 } scale={ 10 }>
        <planeGeometry />
        <meshBasicMaterial color="red" />
    </mesh>
</Environment>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这样就有红色的光映射到其他物体上了
删除了预设更加明显:
image.png
默认情况下,环境映射的背景是黑色的,这就是为什么只照亮红色的一面。
我们可以通过在场景中渲染的场景上设置背景颜色来改变这一点 <Environment>

<Environment
    background
>
    <color args={ [ 'blue' ] } attach="background" />
    {/* ... */}
</Environment>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

image.png

[10]Ground

当使用环境贴图作为背景时,我们有一种物体漂浮的感觉,因为图像是无限远的。
通过添加 ground 属性,环境贴图的投影将使对象下方的地板看起来好像很近。
使用以下参数 background 而不是属性添加 ground 属性: <environment>

<Environment
    preset="sunset"
    ground={ {
        height: 7,
        radius: 28,
        scale: 100
    } }
>
    {/* ... */}
</Environment>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image.png
该地面被视为位于场景 0 的高程处。这意味着,从理论上讲,我们的物体在地下。我们可以通过将它们的 position-y 属性向上移动一点来解决这个问题:

<mesh castShadow position-y={ 1 } position-x={ - 2 }>
    {/* ... */}
</mesh>

<mesh castShadow ref={ cube } position-y={ 1 } position-x={ 2 } scale={ 1.5 }>
    {/* ... */}
</mesh>

<mesh receiveShadow position-y={ 0 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
    {/* ... */}
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

image.png
我们实际上可以删除或注释绿色地板平面,并使用 其 position 属性向上移动:

<ContactShadows
    position={ [ 0, 0, 0 ] }
    scale={ 10 }
    resolution={ 512 }
    far={ 5 }
    color={ color }
    opacity={ opacity }
    blur={ blur }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

通过控件调整

const { envMapIntensity, envMapHeight, envMapRadius, envMapScale } = useControls('environment map', {
    envMapIntensity: { value: 7, min: 0, max: 12 },
    envMapHeight: { value: 7, min: 0, max: 100 },
    envMapRadius: { value: 28, min: 10, max: 1000 },
    envMapScale: { value: 100, min: 10, max: 1000 }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
<Environment
    preset="sunset"
    ground={ {
        height: envMapHeight,
        radius: envMapRadius,
        scale: envMapScale
    } }
>
</Environment>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

[11]Stage

我们做了很多配置。虽然它不是太复杂,但有时,我们只想要一个默认的好看的设置和最少的配置。
这就是 Stage 帮助者所做的。
Stage 将为我们设置环境地图、阴影和两个方向光。它还将场景居中。如果您想要快速简便的设置,这是一个很好的解决方案。
为了使事情变得简单并且不丢失我们之前的设置,我们将注释 JSX 中除 <OrbitControls><Perf> (甚至 ) <mesh> 之外的所有内容。

{/* <BakeShadows /> */}

<Perf position="top-left" />

  <OrbitControls makeDefault />

{/* <Environment
    background
    // files={ [
    //     './environmentMaps/2/px.jpg',
    //     './environmentMaps/2/nx.jpg',
    //     './environmentMaps/2/py.jpg',
    //     './environmentMaps/2/ny.jpg',
    //     './environmentMaps/2/pz.jpg',
    //     './environmentMaps/2/nz.jpg',
    // ] }
    // files="./environmentMaps/the_sky_is_on_fire_2k.hdr"
    preset="sunset"
    resolution={ 32 }
    ground={ {
        height: envMapHeight,
        radius: envMapRadius,
        scale: envMapScale
    } }
>
</Environment> */}

{/* <Sky sunPosition={ sunPosition } /> */}

{/* <directionalLight
    ref={ directionalLight }
    position={ sunPosition }
    intensity={ 1.5 }
    castShadow
    shadow-mapSize={ [ 1024, 1024 ] }
    shadow-camera-near={ 1 }
    shadow-camera-far={ 10 }
    shadow-camera-top={ 5 }
    shadow-camera-right={ 5 }
    shadow-camera-bottom={ - 5 }
    shadow-camera-left={ - 5 }
/> */}
{/* <ambientLight intensity={ 0.5 } /> */}

{/* <AccumulativeShadows
    position={ [ 0, - 0.99, 0 ] }
    scale={ 10 }
    color="#316d39"
    opacity={ 0.8 }
    frames={ Infinity }
    temporal
    blend={ 100 }
>
    <RandomizedLight
        amount={ 8 }
        radius={ 1 }
        ambient={ 0.5 }
        intensity={ 1 }
        position={ [ 1, 2, 3 ] }
        bias={ 0.001 }
    />
</AccumulativeShadows> */}

{/* <mesh castShadow position-y={ 1 } position-x={ - 2 }>
    <sphereGeometry />
    <meshStandardMaterial color="orange" envMapIntensity={ envMapIntensity } />
</mesh> */}

{/* <mesh castShadow ref={ cube } position-y={ 1 } position-x={ 2 } scale={ 1.5 }>
    <boxGeometry />
    <meshStandardMaterial color="mediumpurple" envMapIntensity={ envMapIntensity } />
</mesh> */}

{/* <mesh receiveShadow position-y={ 0 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
    <planeGeometry />
    <meshStandardMaterial color="greenyellow" envMapIntensity={ envMapIntensity } />
</mesh> */}

{/* <ContactShadows
    position={ [ 0, 0, 0 ] }
    scale={ 10 }
    resolution={ 512 }
    far={ 5 }
    color={ color }
    opacity={ opacity }
    blur={ blur }
/> */}

<mesh castShadow position-y={ 1 } position-x={ - 2 }>
  <sphereGeometry />
  <meshStandardMaterial color="orange" envMapIntensity={ envMapIntensity } />
</mesh>

  <mesh castShadow ref={ cube } position-y={ 1 } position-x={ 2 } scale={ 1.5 }>
  <boxGeometry />
  <meshStandardMaterial color="mediumpurple" envMapIntensity={ envMapIntensity } />
  </mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

image.png
导入 Stage 自 @react-three/drei :

import { Stage, Environment, Sky, ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
  • 1

现在将两者 包装起来 :

<Stage>
  <mesh position-y={ 1 } position-x={ - 2 }>
    <sphereGeometry />
    <meshStandardMaterial color="orange" />
  </mesh>

  <mesh ref={ cube } position-y={ 1 } position-x={ 2 } scale={ 1.5 }>
    <boxGeometry />
    <meshStandardMaterial color="mediumpurple" />
  </mesh>
</Stage>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我们仍然可以使用以下 contactShadows 属性来控制接触阴影:

<Stage
    contactShadow={ { opacity: 0.2, blur: 3 } }
>
  • 1
  • 2
  • 3

选择不同的环境贴图预设:

<Stage
    contactShadow={ { opacity: 0.2, blur: 3 } }
    environment="sunset"
>
  • 1
  • 2
  • 3
  • 4

更改方向灯 preset ( ‘rembrandt’ , ‘portrait’ , ‘upfront’ , ): ‘soft’

<Stage
    contactShadow={ { opacity: 0.2, blur: 3 } }
    environment="sunset"
    preset="portrait"
>
  • 1
  • 2
  • 3
  • 4
  • 5

更改照明强度:

<Stage
    contactShadow={ { opacity: 0.2, blur: 3 } }
    environment="sunset"
    preset="portrait"
    intensity={ 2 }
>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.Load models with R3F

export default function Experience()
{
    return <>

        <Perf position="top-left" />

        <OrbitControls makeDefault />

        <directionalLight castShadow position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
        <ambientLight intensity={ 0.5 } />

        <mesh receiveShadow position-y={ - 1 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
            <planeGeometry />
            <meshStandardMaterial color="greenyellow" />
        </mesh>

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

image.png

[1]Loading a model

R3F 提供了一个名为 useLoader abstract loading 的钩子。

import { useLoader } from '@react-three/fiber'
  • 1

要使用它,我们需要向它发送我们想要使用的Three.js加载程序类和文件的路径。

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
  • 1
export default function Experience()
{
    const model = useLoader(GLTFLoader, './hamburger.glb')
    console.log(model)

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在场景中放置,我们需要的是 是某种我们想要放入其中的容器。
它不是我们在场景中能够看到的真实对象,但它是 R3F 支持的容器,它将处理和显示我们在其属性中 object 放置的任何内容。

export default function Experience()
{
    const model = useLoader(GLTFLoader, './hamburger.glb')

    return <>

        {/* ... */}

        <primitive object={ model.scene } scale={ 0.35 } />

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image.png

[2]DRACO

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
  • 1
const model = useLoader(
    GLTFLoader,
    './hamburger-draco.glb',
    (loader) =>
    {
        const dracoLoader = new DRACOLoader()
        dracoLoader.setDecoderPath('./draco/')
        loader.setDRACOLoader(dracoLoader)
    }
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image.png

[3]延迟加载

加载大模型时推荐使用延时加载

Suspense

要实现延迟加载,我们可以使用标签 <Suspense>
<Suspense> 是一个 React 组件,它将等待过程完成(在我们的例子中加载模型)然后再渲染组件。
这样就需要多创建一个组件来渲染加载中的显示:
我们需要将模型放在一个单独的组件中。

export default function Model()
{
    return null
}
  • 1
  • 2
  • 3
  • 4
export default function Model()
{
    const model = useLoader(
        GLTFLoader,
        './FlightHelmet/glTF/FlightHelmet.gltf',
        (loader) =>
        {
            const dracoLoader = new DRACOLoader()
            dracoLoader.setDecoderPath('./draco/')
            loader.setDRACOLoader(dracoLoader)
        }
    )

    return return <primitive object={ model.scene } scale={ 5 } position-y={ - 1 } />
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

image.png
在 Experience.jsx 中,导入 Suspense 自 react :

import { Suspense } from 'react'
  • 1
<Suspense>
  <Model />
</Suspense>
  • 1
  • 2
  • 3

这样就实现了加载完成后再渲染该组件了

Fallback

Fallback 是用户在组件未准备就绪时(在我们的例子中,在模型加载时)将看到的内容。
要定义回退,我们可以使用以下 fallback 属性:

<Suspense
    fallback={  }
>
  • 1
  • 2
  • 3

我们可以设置一个 :

<Suspense
    fallback={
      <mesh position-y={ 0.5 } scale={ [ 2, 3, 2 ] }>
        <boxGeometry args={ [ 1, 1, 1, 2, 2, 2 ] } />
        <meshBasicMaterial wireframe color="red" />
      </mesh>
    }
    >
  <Model />
</Suspense>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

它已经更好了,但让我们更进一步,创建一个组件。

export default function Placeholder()
{
    return <mesh position-y={ 0.5 } scale={ [ 2, 3, 2 ] }>
        <boxGeometry args={ [ 1, 1, 1, 2, 2, 2 ] } />
        <meshBasicMaterial wireframe color="red" />
    </mesh>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
<Suspense fallback={ <Placeholder /> }>
  • 1

[4]GLTF loading with drei

多亏了 drei,还有一种更简单的方法。
Drei 实现了多个加载器帮助程序,如 useGLTFuseFBX
在 Model.jsx 中,导入 useGLTF 自 @react-three/drei :

import { useGLTF } from '@react-three/drei'
  • 1

现在,将整个 useLoader() 调用 useGLTF() 替换为文件的路径作为唯一参数:

export default function Model()
{
    const model = useGLTF('./hamburger.glb')

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

就是这样,我们可以删除 useLoader 、 GLTFLoader 和 DRACOLoader 的导入。对 dracoLoader 也不需要了

const model = useGLTF('./hamburger-draco.glb')
  • 1

image.png

[5]Preloading

目前,我们的模型只有在组件实例化时才会开始加载。
我们可以使用 上useGLTF 的 preload方法做到这一点。
在 Model.jsx 文件中,在 Model 函数之后,使用模型 URL 调用: preload

export default function Hamburger({ ...props })
{
    // ...
}

useGLTF.preload('./hamburger-draco.glb')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

[6]Multiple instances

在我们的例子中,如果我们想要第二个汉堡包怎么办?还是三个?还是一百个?
Drei 通过 Clone 助手使这成为可能。
在 Model.jsx 中 @react-three/drei :

import { Clone, useGLTF } from '@react-three/drei'
  • 1
export default function Model()
{
    // ...
    
    return <>
        <Clone object={ model.scene } scale={ 0.35 } position-x={ - 4 } />
        <Clone object={ model.scene } scale={ 0.35 } position-x={ 0 } />
        <Clone object={ model.scene } scale={ 0.35 } position-x={ 4 } />
    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image.png
如果检查性能监视,则会发现几何图形和着色器的数量保持不变。 Clone 创建多个网格,它仍然基于相同的几何形状和材料。

[7]GLTF to component

如果我们想操纵汉堡包的不同部分,我们需要遍历加载的模型,寻找合适的子项,以某种方式保存它,并应用我们需要的任何东西。
另一种选择是在 3D 软件中打开它,更改它并再次导出。
这些解决方案都不方便。
将我们的汉堡包作为一个组件提供,其中包含我们可以随心所欲地操作的简单 JSX 中的所有内容,这不是很棒吗?
这就是 GLTF -> React Three Fiber 所做的。
There is a command-line tool available here https://github.com/pmndrs/gltfjsx
And an online version available here https://gltf.pmnd.rs/
image.png
把代码拷贝使用:

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/

import React, { useRef } from "react";
import { useGLTF } from "@react-three/drei";

export function Model(props) {
  const { nodes, materials } = useGLTF("/hamburger.glb");
  return (
    <group {...props} dispose={null}>
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.bottomBun.geometry}
        material={materials.BunMaterial}
      />
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.meat.geometry}
        material={materials.SteakMaterial}
        position={[0, 2.82, 0]}
      />
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.cheese.geometry}
        material={materials.CheeseMaterial}
        position={[0, 3.04, 0]}
      />
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.topBun.geometry}
        material={materials.BunMaterial}
        position={[0, 1.77, 0]}
      />
    </group>
  );
}

useGLTF.preload("/hamburger.glb");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

[8]Refactoring 重构

这个组件几乎可以使用了,但我们需要做一些重构。

// ...

export default function Hamburger(props) {

// ...
  • 1
  • 2
  • 3
  • 4
  • 5
import Hamburger from './Hamburger.jsx'
  • 1
export default function Experience()
{
    return <>

        {/* ... */}

        <Suspense fallback={ <Placeholder position-y={ 0.5 } scale={ [ 2, 3, 2 ] } /> }>
            <Hamburger scale={ 0.35 } />
        </Suspense>

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

由于模型的每个部分都写在 中 Hamburger.jsx ,我们可以对它有更多的控制权。
举个例子,我们可以通过直接改变顶部发髻的位置来移动它(最后一个): Hamburger.jsx

export default function Hamburger(props) {
  const { nodes, materials } = useGLTF("./hamburger.glb");
  return (
    <group {...props} dispose={null}>
      {/* ... */}
      <mesh
        castShadow
        receiveShadow
        geometry={nodes.topBun.geometry}
        material={materials.BunMaterial}
        position={[0, 3.77, 0]}
      />
    </group>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

image.png

[9]修复阴影

阴影看起来有点奇怪,条纹穿过汉堡包的表面。
这被称为阴影痤疮,这是由于模型在自身上投射阴影。
我们可以通过调整 bias or shadowBias 中的定向光影来解决这个问题 Experience.jsx :

<directionalLight castShadow position={ [ 1, 2, 3 ] } intensity={ 1.5 } shadow-normalBias={ 0.04 } />
  • 1

[10] 动画

We are going to use the usual animated Fox provided by the Kronos Group in the glTF-Sample-Models GitHub repository.

import { useGLTF } from '@react-three/drei'

export default function Fox()
{
   const fox = useGLTF('./Fox/glTF/Fox.gltf')

   return <primitive
    object={ fox.scene }
    scale={ 0.02 }
    position={ [ - 2.5, 0, 2.5 ] }
    rotation-y={ 0.3 }
/>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
import Fox from './Fox.jsx'
export default function Experience()
{
    return <>

        {/* ... */}

        <Fox />

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
Play the animation
import { useAnimations, useGLTF } from '@react-three/drei'
  • 1
export default function Fox()
{
    const fox = useGLTF('./Fox/glTF/Fox.gltf')
    console.log(fox)

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

image.png

export default function Fox()
{
    const fox = useGLTF('./Fox/glTF/Fox.gltf')
    const animations = useAnimations(fox.animations, fox.scene)
    console.log(animations)

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

image.png
现在,我们可以访问模型提供的各种动画,并且每个动画都已使用动画的名称( Run , SurveyWalk 如果是 Fox)转换为 AnimationAction,并且这些操作在 animation.actions 对象中可用。
但是在开始任何这些操作之前,最好在组件首次完成渲染后执行此操作,我们可以使用 useEffect .

import { useEffect } from 'react'
  • 1
export default function Fox()
{
    const fox = useGLTF('./Fox/glTF/Fox.gltf')
    const animations = useAnimations(fox.animations, fox.scene)
    console.log(animations)

    useEffect(() =>
    {
        const action = animations.actions.Run
        action.play()
    }, [])

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这样就可以动了
React Three Fiber 和 useAnimations 助手将负责更新每一帧的动画。
如果你想让狐狸在几秒钟后开始走路,你可以使用 AnimationAction 中可用的各种方法,比如 crossFadeFrom 去 fadeOutRun 和 fadeInWalk :

useEffect(() =>
{
    animations.actions.Run.play()

    window.setTimeout(() =>
    {
        animations.actions.Walk.play()
        animations.actions.Walk.crossFadeFrom(animations.actions.Run, 1)
    }, 2000)
}, [])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
动画控制和清理阶段

使用 leva 控制

import { useControls } from 'leva'
  • 1

在 Fox 函数中,在 之后 useAnimations ,调用 useControls 以创建一个带有 a 的调整,其中选项是模型中的可用动画。

export default function Fox()
{
    const fox = useGLTF('./Fox/glTF/Fox.gltf')
    const animations = useAnimations(fox.animations, fox.scene)

    const { animationName } = useControls({
        animationName: { options: animations.names }
    })

  useEffect(() =>{
      const action = animations.actions[animationName]
      action.play()
  }, [])

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

目前, useEffect dependencies 数组为空,这意味着该函数在第一次渲染后只会被调用一次。这正是设置特定依赖项的用武之地。我们希望在第一次渲染时调用该函数,但也希望在 animationName 更改时调用该函数。
将 animationName 添加到 dependencies 数组:

useEffect(() =>
{
    // ...
}, [ animationName ])
  • 1
  • 2
  • 3
  • 4

当我们更改 animationName 时,该函数被调用,但是当我们将其更改为第二个动画后,狐狸的动画看起来很奇怪,如果我们将其更改为第三个动画,则更奇怪。
原因是所有动画都在一起播放,Three.js会将它们混合在一起。首先,您看到的 Survey 是动画;然后是 Walk 动画和动画的混合 Survey ;最后是 Survey 、 和 WalkRun 动画的混合。
为了解决这个问题,我们需要逐步停止旧动画 ( fadeOut ) 并逐步启动新动画 ( fadeIn )。
然后,我们不是仅仅调用 play() ,而是先通过在 play 之前添加 fadeIn 一个值 0.5 (以秒为单位的 fadeIn 持续时间) 来淡入它:

useEffect(() =>
{
    const action = animations.actions[animationName]
    action.fadeIn(0.5).play()
}, [ animationName ])
  • 1
  • 2
  • 3
  • 4
  • 5

return 中销毁:

useEffect(() =>
{
    const action = animations.actions[animationName]
    action.fadeIn(0.5).play()


    return () =>
    {
        action.fadeOut(0.5)
    }
}, [ animationName ])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

或者:

useEffect(() =>
{
    const action = animations.actions[animationName]
    action
        .reset()
        .fadeIn(0.5)
        .play()

    // ...
}, [ animationName ])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

6.3D Text with R3F

import { Text3D, OrbitControls } from '@react-three/drei'
  • 1

下载字体:
如果需要,您可以使用此网站创建自己的字体 http://gero3.github.io/facetype.js/

export default function Experience()
{
    return <>

        <Perf position="top-left" />

        <OrbitControls makeDefault />

        <Text3D font="./fonts/helvetiker_regular.typeface.json">
            HELLO R3F
            <meshNormalMaterial />
        </Text3D>

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

image.png

[1]Centering

import { Center, Text3D, OrbitControls } from '@react-three/drei'
  • 1
<Center>
  <Text3D font="./fonts/helvetiker_regular.typeface.json">
    HELLO R3F
    <meshNormalMaterial />
  </Text3D>
</Center>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

image.png
配置参数,好看点:

<Center>
  <Text3D
    font="./fonts/helvetiker_regular.typeface.json"
    size={ 0.75 }
    height={ 0.2 }
    curveSegments={ 12 }
    bevelEnabled
    bevelThickness={ 0.02 }
    bevelSize={ 0.02 }
    bevelOffset={ 0 }
    bevelSegments={ 5 }
    >
    HELLO R3F
    <meshNormalMaterial />
  </Text3D>
</Center>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

image.png

[2]Matcap Material

首先,我们需要加载 matcap 纹理
我们将使用一个名为 useMatcapTexture drei 的助手,它将自动从此存储库加载纹理https://github.com/emmelleppi/matcaps.

import { useMatcapTexture, Center, Text3D, OrbitControls } from '@react-three/drei'
  • 1

image.png

7.Portal Scene withR3F

[1] 使用 geometry 作为 mesh 的 geometry

因为需要给模型设置不同的材质,所以不直接将整个模型添加到场景

const { nodes } = useGLTF('./model/portal.glb')
console.log(nodes)
  • 1
  • 2

image.png
添加 geometry:

export default function Experience()
{
    // ...

    return <>

        {/* ... */}

        <mesh geometry={ nodes.baked.geometry } />

    </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

image.png
加载这个模型的纹理进行贴图

import { useTexture, useGLTF, OrbitControls } from '@react-three/drei'
  • 1
export default function Experience()
{
    // ...

   const bakedTexture = useTexture('./model/baked.jpg')
    bakedTexture.flipY = false
    console.log(bakedTexture)

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

image.png

<mesh geometry={ nodes.baked.geometry }>
  <meshBasicMaterial map={ bakedTexture } />
</mesh>
  • 1
  • 2
  • 3

image.png
定个心:

<Center>
  <mesh geometry={ nodes.baked.geometry }>
    <meshBasicMaterial map={ bakedTexture } />
  </mesh>
</Center>
  • 1
  • 2
  • 3
  • 4
  • 5

[2] 使用着色器材质

<mesh geometry={ nodes.portalLight.geometry } position={ nodes.portalLight.position } rotation={ nodes.portalLight.rotation }>
  <shaderMaterial />
</mesh>
  • 1
  • 2
  • 3
import portalVertexShader from './shaders/portal/vertex.glsl'
import portalFragmentShader from './shaders/portal/fragment.glsl'
console.log(portalFragmentShader)
  • 1
  • 2
  • 3
<shaderMaterial
  vertexShader={ portalVertexShader }
  fragmentShader={ portalFragmentShader }
  />
  • 1
  • 2
  • 3
  • 4

传递变量;

<shaderMaterial
  vertexShader={ portalVertexShader }
  fragmentShader={ portalFragmentShader }
  uniforms={ {
    uTime: { value: 0 },
    uColorStart: { value: new THREE.Color('#ffffff') },
    uColorEnd: { value: new THREE.Color('#000000') }
  } }
  />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

[3]shaderMaterial helper

,drei 提供了一个名为 shaderMaterial helper 的帮助程序,用于创建一个 ShaderMaterial,然后我们将在 JSX 中提供该 ShaderMaterial,简化了数据传递过程

import { shaderMaterial, Sparkles, Center, useTexture, useGLTF, OrbitControls } from '@react-three/drei'
  • 1

然后,在 Experience 函数之前调用它,并在 PortalMaterial 变量中分配结果:

const PortalMaterial = shaderMaterial(
    {
        uTime: 0,
        uColorStart: new THREE.Color('#ffffff'),
        uColorEnd: new THREE.Color('#000000')
    },
    portalVertexShader,
    portalFragmentShader
)

export default function Experience()
{
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

现在,为了将它转换为我们可以在 JSX 中使用的 R3F 标签,我们将使用 extend

import { extend } from '@react-three/fiber'
  • 1
const PortalMaterial = shaderMaterial(
    // ...
)

extend({ PortalMaterial })
  • 1
  • 2
  • 3
  • 4
  • 5
<mesh geometry={ nodes.portalLight.geometry } position={ nodes.portalLight.position } rotation={ nodes.portalLight.rotation }>
  <portalMaterial />
</mesh>
  • 1
  • 2
  • 3

[4] 动画

更新 uTime ,需要引用 jsx 中的 ,那么就需要使用 ref

import { useRef } from 'react'
import { useFrame, extend } from '@react-three/fiber'
export default function Experience()
{
    // ...

   const portalMaterial = useRef()
    useFrame((state, delta) =>{
      portalMaterial.current.uTime += delta
    })

    // ...
  return (
    <portalMaterial ref={ portalMaterial } />
  )
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

8.鼠标事件

[1]侦听点击事件

在原生 threejs 中使用点击事件需要使用光线投射来实现,而这里并不需要
我们需要做的就是为场景中的对象添加一个 onClick 属性(如 ),并为其提供一个函数

export default function Experience()
{
    // ...

    const eventHandler = () =>
    {
        console.log('the event occured')
    }

    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
<mesh ref={ cube } position-x={ 2 } scale={ 1.5 } onClick={ eventHandler } >
  <boxGeometry />
  <meshStandardMaterial color="mediumpurple"/>
</mesh>
  • 1
  • 2
  • 3
  • 4

点击改变颜色使用 ref 引用

const eventHandler = () =>
{
    cube.current.material.color.set(`hsl(${Math.random() * 360}, 100%, 75%)`)
}
  • 1
  • 2
  • 3
  • 4

[2] 活动信息

打印事件参数:
image.png

const eventHandler = (event) =>
{
    console.log(event)
    cube.current.material.color.set(`hsl(${Math.random() * 360}, 100%, 75%)`)
}
  • 1
  • 2
  • 3
  • 4
  • 5
console.log('---')
console.log('distance', event.distance) // Distance between camera and hit point
console.log('point', event.point) // Hit point coordinates (in 3D)
console.log('uv', event.uv) // UV coordinates on the geometry (in 2D)
console.log('object', event.object) // The object that triggered the event
console.log('eventObject', event.eventObject) // The object that was listening to the event (useful where there is objects in objects)

console.log('---')
console.log('x', event.x) // 2D screen coordinates of the pointer
console.log('y', event.y) // 2D screen coordinates of the pointer

console.log('---')
console.log('shiftKey', event.shiftKey) // If the SHIFT key was pressed
console.log('ctrlKey', event.ctrlKey) // If the CTRL key was pressed
console.log('metaKey', event.metaKey) // If the COMMAND key was pressed
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

[3]Other events

除了 onClick 还有:
onContextMenu 在上下文菜单应出现时触发。
onDoubleClick:双击事件
onPointerUp:鼠标松开
onPointerDown
onPointerOver /onPointerEnter:当光标或手指刚好位于对象上方时,将触发该事件。
onPointerOut 和 onPointerLeave:
onPointerMove
onPointerMissed

[4]Occluding

当出现物体被前面物体遮挡了点击仍然有效时可以设置:

<mesh position-x={ - 2 } onClick={ (event) => event.stopPropagation() }>
    {/* ... */}
</mesh>
  • 1
  • 2
  • 3

[5]Cursor

监听鼠标进入和离开三维对象:

<mesh
  ref={ cube }
  position-x={ 2 }
  scale={ 1.5 }
  onClick={ eventHandler }
  onPointerEnter={ () => {  } }
  onPointerLeave={ () => {  } }
  >
  <boxGeometry />
  <meshStandardMaterial color="mediumpurple"/>
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

改变光标的样式:

<mesh
  ref={ cube }
  position-x={ 2 }
  scale={ 1.5 }
  onClick={ eventHandler }
  onPointerEnter={ () => { document.body.style.cursor = 'pointer' } }
  onPointerLeave={ () => { document.body.style.cursor = 'default' } }
  >
  <boxGeometry />
  <meshStandardMaterial color="mediumpurple"/>
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

[6] 复杂对象上的事件

加载个多结构的对象:

export default function Experience()
{
  // ...
  const hamburger = useGLTF('./hamburger.glb')

  return <>

    {/* ... */}

    <primitive
      object={ hamburger.scene }
      scale={ 0.25 }
      position-y={ 0.5 }
      />

  </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

由于 是 object 的简单占位符,我们可以像听任何其他对象一样监听它上面的事件

<primitive
  object={ hamburger.scene }
  scale={ 0.25 }
  position-y={ 0.5 }
  onClick={ (event) =>
    {
      console.log('click')
    } }
  /
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image.png
每次点击最多可以得到 4 个 console
这是因为光线同时穿过多个物体。
即使我们在父母身上听事件,R3F 实际上也会测试孩子,这是有充分理由的,因为没有孩子对象,父母什么都不是
我们实际上可以使用以下 object 属性来测试哪些对象触发了它 event :

<primitive
    object={ hamburger.scene }
    scale={ 0.25 }
    position-y={ 0.5 }
    onClick={ (event) =>
    {
        console.log(event.object)
    } }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

image.png
我们可以使用 stopPropagation 方法来停止event传播 :

<primitive
    object={ hamburger.scene }
    scale={ 0.25 }
    position-y={ 0.5 }
    onClick={ (event) =>
    {
        console.log(event.object)
        event.stopPropagation()
    } }
/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这样打印的就只有一个了

[7] 性能

点击事件对 CPU 来说是一项相当繁重的任务。

一般优化

尽量减少侦听事件的对象数量,并避免测试复杂的几何图形。如果你在互动时发现冻结,即使是短暂的冻结,你也会有更多的优化工作要做。

meshBounds

我们可以应用的一个简单的优化是 drei 的 meshBounds 助手。
此帮助程序将在网格周围创建一个理论球体(称为边界球体),并且指针事件将在该球体上进行测试,而不是测试网格的几何形状
如果您不需要对复杂的几何形状进行非常精确的检测,这将非常有用(用来替代判断)

import { meshBounds, useGLTF, OrbitControls } from '@react-three/drei'
  • 1
<mesh
    ref={ cube }
    raycast={ meshBounds }
    position-x={ 2 }
    scale={ 1.5 }
    onClick={ eventHandler }
    onPointerEnter={ () => { document.body.style.cursor = 'pointer' } }
    onPointerLeave={ () => { document.body.style.cursor = 'default' } }
>
    {/* ... */}
</mesh>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
BVH

如果您有非常复杂的几何图形,并且仍然需要指针事件准确,则还可以使用 BVH(边界体积层次结构)
这是一种更复杂的方法,但有了 drei 的 useBVH 助手,它变得容易了。

9. 后处理

后处理也受益于 React 和 R3F 系统,因为它更容易实现,但在某些方面也得到了优化

[1] 后处理的问题

在原生 threejs 中我们通过后处理通道来实现后处理效果,乒乓缓冲

[2] 实现

import { EffectComposer } from '@react-three/postprocessing'
  • 1

虽然它与 EffectComposer 我们在原生Three.js中使用的名称相同,但它不是同一个类。

export default function Experience()
{
  return <>

    <EffectComposer>
    </EffectComposer>

    {/* ... */}

  </>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
多重采样

默认情况下,它的值为 at 8 ,我们可以将其降低到 0 以完全禁用它。

<EffectComposer multisampling={ 0 }>
</EffectComposer>
  • 1
  • 2

禁用多重采样时,性能应该会更好

寻找效果以及如何实现它们

Post Processing

React-postprocessing

晕影效果
import { Vignette, EffectComposer } from '@react-three/postprocessing'
  • 1
<EffectComposer>
    <Vignette
    offset={ 0.3 }
    darkness={ 0.9 }
/>
</EffectComposer>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

image.png

Blending

blendFunction 工作方式有点像您可以在图像编辑软件(如 Photoshop)中找到的混合。这就是我们所画的颜色与后面的颜色融合的方式。

。。。。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/228492
推荐阅读
相关标签
  

闽ICP备14008679号