https://threejs-journey.com/lessons/what-are-react-and-react-three-fiber#学习笔记
填充模版
每帧都会执行的函数
React Three Fiber Documentation
示例:使用 useRef+useFrame 实现立方体动画:
但是直接这样写有一个问题,因为这是基于帧数的动画,每帧动一点,那么在不同帧率的电脑上动画的快慢将不一样
这时就可以使用 useFrame 自带的函数参数了来解决这个问题,第二个参数,通常称为 delta,为之上一帧到这一帧所花费的时间,基本上是一个定数,越高帧数的电脑的更新的要频繁,只越小,那么就可以:
而这个 state 则可以获取当前场景中的很多对象,如 webgl、camera 等很多全局对象,但有时候我们不需要每帧获取这些对象,只需要获取一次即可,那这时就可以使用另外一个 hook:useThree
这个 hook 可以为我们提供相同的 state,但仅仅在组件初始化时提供一次
这里通过使用 extend 和 three 原生的 ortb 来构建一个鼠标控制器(先不使用官方自带的)
也很简单,通过查看 three.js 文档获取到 orbitControls 需要的参数
但是直接在组件内构建创建顶点,如果有 state 数据,每当改变时代码将被重新执行,将重新创建顶点,这里就需要注意了:
还可以使用 useEffect()来计算顶点法线
默认为透视相机:
相机环绕动画模板:
默认是采用了色调映射的,如果不需要可以取消掉
默认是采用的 ACES 色调映射
React Three Fiber 的核心特性就是复用,而其拥有很多复原的库、组件,有数百个 如:
默认是平移变换,还可以改为旋转变换:
还可以改为 scale 缩放变换,默认为 translate
还有有一种使用方式是使用 ref 进行父级关联:
这种方式更好
还有一个 bug,就是相机会跟随移动:这时只需要加上这一句就可以了:
TransformControls
的一个替代方案,对于用户比较友好,而TransformControls
对于开发者友好
pivotcontrols 不能像transformcontrols那样作为一个组工作如果我们希望它位于球体的中心,我们必须改变它的位置使用 anchor 属性
这个 anchor 为这个对象的相对位置
有很多个性化属性:如配置颜色等:
单独放置:
放置到现有对象上面:
自定义 css,构建一个 class
然后再 css 文件中配置即可
距离函数
天机属性:
反射材质,只适用于平面网格几何
默认情况下:
安装 npm i leva
每当这个 dubug 值被改变时,这个组件将 rerender,组件内的代码将重新执行
这就是它的工作原理
添加范围:
矢量:
import {button} from useCcontrol
其还有一些属性,初始化属性
注意需要添加到 Canvas 外边
npm install r3f-perf@6.5
import { Perf } from 'r3f-perf'
添加到 Canvas 内部:
export default function Experience()
{
// ...
return <>
<Perf />
{/* ... */}
</>
}
默认情况下,接口位于右上角,这与 Leva 冲突,但我们可以使用属性 position 进行更改:
<Perf position="top-left" />
R3F-Perf 显示大量有用的信息。
我们甚至可以访问绘制调用的数量、内存使用情况、渲染场景所需的时间等。
const { perfVisible } = useControls({
perfVisible: true
})
{ perfVisible && <Perf position="top-left" /> }
如果想要的只是均匀的颜色,那么这些技术中的任何一种都是可行的解决方案
html,
body,
#root
{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: red;
}
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>
)
const created = ({ gl }) =>
{
gl.setClearColor('#ff0000', 1)
}
同样效果
import * as THREE from 'three'
// ...
const created = ({ scene }) =>
{
scene.background = new THREE.Color('#ff0000')
}
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(
)
R3F 支持所有默认Three.js灯:
export default function Experience()
{
const directionalLight = useRef()
// ...
}
<directionalLight ref={ directionalLight } position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
import { useHelper, OrbitControls } from '@react-three/drei'
第一个参数是对光源的 useHelper 引用,第二个参数是我们想要从Three.js使用的帮助程序类。
这意味着我们首先需要导入 THREE 才能访问该 DirectionalLightHelper 类:
import * as THREE from 'three'
export default function Experience()
{
const directionalLight = useRef()
useHelper(directionalLight, THREE.DirectionalLightHelper, 1)
// ...
}
useHelper 不仅仅是为了光线,因为我们可以将其用于相机 CameraHelper 作为示例。
我们将从默认的 Three.js 阴影系统开始,但由于 R3F 和 drei,我们将看到其他阴影解决方案变得更加容易。
要开启WebGLRenderer的阴影渲染 ,我们需要做的就是在 中 index.js 添加一个 shadows 属性:
root.render(
<Canvas
shadows
camera={ {
fov: 45,
near: 0.1,
far: 50,
position: [ - 4, 3, 6 ]
} }
>
<Experience />
</Canvas>
)
回到 Experience.js ,我们添加 castShadow
:
<directionalLight ref={ directionalLight } castShadow position={ [ 1, 2, 3 ] } intensity={ 1.5 } />
添加 castShadow 球体 和立方体 (这些对象只需要投射阴影,因为上面什么都没有):
<mesh castShadow position-x={ - 2 }>
{/* ... */}
</mesh>
<mesh castShadow position-x={ 2 } scale={ 1.5 }>
{/* ... */}
</mesh>
最后,在地板上 添加 receiveShadow (地板只需要接收阴影,因为下面什么都没有):
<mesh receiveShadow position-y={ - 1 } rotation-x={ - Math.PI * 0.5 } scale={ 10 }>
{/* ... */}
</mesh>
如果我们的场景是静态的(事实并非如此,因为立方体在旋转,但我们无论如何都会这样做),我们可以添加来自 drei 的 BakeShadows 帮助程序。
这将只渲染一次阴影,而不是在每一帧上渲染。
导入 BakeShadows 自 @react-three/drei :
import { BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
export default function Experience()
{
// ...
return <>
<BakeShadows />
{/* ... */}
</>
}
立方体阴影中看到的那样,阴影不会在每一帧上更新,从而提高了性能
默认情况下,阴影贴图分辨率相当低,以保持稳定的性能。
在纯 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 ] }
/>
我们可以对 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 }
/>
阴影被剪切是因为值太小,但这里的主要目的是解释如何调整这些阴影。
调整下:
<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 }
/>
默认阴影太清晰。有多种方法可以软化它们,我们将发现一种称为百分比更近柔和阴影 (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()
{
// ...
}
softShadows() 参数直接用于编译着色器中,修改它们意味着重新编译所有材质着色器,这就是为什么我们不能实时更改它或将它们添加到调试 UI 中的原因
顾名思义,将 AccumulativeShadows 累积多个阴影渲染,我们将在每次渲染之前随机移动光线。这样,阴影将由一堆不同角度的渲染组成,使其看起来柔和且非常逼真。
AccumulativeShadows 将在平面上渲染。它限制了它的使用,但它在场景的地板上看起来非常好。
地板取消receiveShadow
{/* ... */}
import { AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
需要配置属性
具有多个属性来控制灯光的行为:
amount
:多少盏灯(默认有多盏灯)radius
:抖动的幅度intensity
:灯光的强度ambient
:就像全局光照亮整个场景一样,只使狭窄的空间和缝隙接收阴影以及与阴影贴图相关的参数:
找到最好的调整很复杂,这就是像 Leva 这样的调试 UI 会非常有帮助的地方。
但是让我们偷懒一点,把以下参数放进去:
<RandomizedLight
amount={ 8 }
radius={ 1 }
ambient={ 0.5 }
intensity={ 1 }
position={ [ 1, 2, 3 ] }
bias={ 0.001 }
/>
回到我们还可以访问一些属性 的地方:
由于我们有一个绿色的地板,让我们将阴影设置为深绿色/蓝色, color 并略微减少 opacity :
{/* ... */}
<AccumulativeShadows
position={ [ 0, - 0.99, 0 ] }
scale={ 10 }
color="#316d39"
opacity={ 0.8 }
frames={ 1000 }
>
{/* ... */}
</AccumulativeShadows>
1000 很多,但正如你所看到的,阴影看起来非常平滑。问题在于,Three.js必须在第一帧上一次性完成这些 1000 渲染,您可能会注意到相当长的冻结时间。
这不一定是什么大不了的,反正 1000 太多了,但我们可以通过以下方式 temporal 防止冻结:
{/* ... */}
正如你所看到的,我们需要很多秒才能恢复我们的阴影,但至少我们没有任何冻结,因为每一帧只做了一个渲染。
您可能已经注意到的另一件事是,如果您移动相机,阴影上会绘制一个奇怪的形状。这是由于定向光助手弄乱了阴影贴图。
您可能已经注意到,阴影似乎没有根据立方体移动,这是真的。
为了更清楚地看到这一点,让我们让立方体左右移动。
在 useFrame 中,检索我们在上一课中所做的操作, clockelapsedTime 并将其分配给一个 time 变量:
useFrame((state, delta) =>
{
const time = state.clock.elapsedTime
// ...
})
现在在立方体位置上使用它, Math.sin() 并添加 2 它,使其保持在当前位置附近:
useFrame((state, delta) =>
{
const time = state.clock.elapsedTime
cube.current.position.x = 2 + Math.sin(time)
// ...
})
阴影似乎移动了一会儿(准确地说是 100 帧),然后停止了。这是因为我们特别要求只 AccumulativeShadows 渲染 100 帧。
解决方案是告诉 AccumulativeShadows 继续渲染阴影,我们可以通过将 frames 属性设置为 Infinity :
<AccumulativeShadows
position={ [ 0, - 0.99, 0 ] }
scale={ 10 }
color="#316d39"
opacity={ 0.8 }
frames={ Infinity }
temporal
>
{/* ... */}
</AccumulativeShadows>
不错,但有点跳跃。我们可以区分淡入和淡出的不同阴影贴图。原因是,当使用 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>
好多了,但越 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
})
// ...
}
最后一个影子解决方案称为 ContactShadows
因为它不依赖于 Three.js 的默认影子系统,所以我们将在 上停用 shadows (您也可以删除该属性):
<Canvas
shadows={ false }
camera={ {
fov: 45,
near: 0.1,
far: 50,
position: [ - 4, 3, 6 ]
} }
>
<Experience />
</Canvas>
并评论或删除 :
{/*
*/}
首先要了解 ContactShadows
的是,它无需光即可工作。很奇怪,对吧?
就像 一样 AccumulativeShadows
,它可以在限制其使用的平面上工作,但在地板上看起来非常好。
ContactShadows
这将使整个场景有点像定向光,但相机取代了地板而不是光线。
然后,它会模糊阴影贴图,使其看起来更好。
import { ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
export default function Experience()
{
// ...
return <>
{/* ... */}
<ContactShadows />
{/* ... */}
</>
}
默认配置并不好用
<ContactShadows
position={ [ 0, - 0.99, 0 ] }
/>
import { useControls } from 'leva'
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 },
})
// ...
}
<ContactShadows
position={ [ 0, - 0.99, 0 ] }
scale={ 10 }
resolution={ 512 }
far={ 5 }
color={ color }
opacity={ opacity }
blur={ blur }
/>
以下参数效果很好:
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 },
})
正如我们之前提到的,它将 ContactShadow 渲染从下方看到的场景,并使用该信息来生成阴影。然后,这个阴影将被模糊。对于小场景,性能应该很好,但对于更复杂的场景,此过程可能太重并导致帧速率下降,尤其是因为它必须在每一帧上完成
幸运的是,有一种方法可以通过将 frames 属性设置为 来 1 烘烤阴影:
<ContactShadows
position={ [ 0, - 0.99, 0 ] }
scale={ 10 }
resolution={ 512 }
far={ 5 }
color={ color }
opacity={ opacity }
blur={ blur }
frames={ 1 }
/>
我们选择的数字对应于将生成阴影的帧数。由于我们只想生成一次,因此我们选择了 1 .
对于简单的对象显示,这很好,但对于更复杂或更逼真的渲染,您最好使用其他阴影解决方案。
为了使场景更加逼真,并为了添加漂亮的自然背部,我们可以使用天空类 https://threejs.org/examples/webgl_shaders_sky.html
与往常一样,R3F 和 drei 通过 Sky 助手使任务变得非常容易。
import { Sky, ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
export default function Experience()
{
// ...
return <>
{/* ... */}
<Sky />
{/* ... */}
</>
}
配置调整太阳的位置:
const { sunPosition } = useControls('sky', {
sunPosition: { value: [ 1, 2, 3 ] }
})
<Sky sunPosition={ 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 }
/>
之前可以使用六张图片和 360 图片做环境贴图,现在 drei 有现成的了
import { Environment, useHelper, OrbitControls } from '@react-three/drei'
首先,我们将使用您可以在 /public/environmentMaps/
文件夹中找到的传统立方体纹理。
将 添加到 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',
] }
/>
{/* ... */}
</>
}
默认值 envMapIntensity 设置为 1
调用 useControls
,将第一个参数设置为 以便 'environment map'
拥有具有该名称的文件夹,并发送一个 envMapIntensity
属性范围介于 和 12 之间的 0 对象(不要忘记检索 envMapIntensity
):
const { envMapIntensity } = useControls('environment map', {
envMapIntensity: { value: 1, min: 0, max: 12 }
})
<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>
上面的只设置了环境贴图,想要设置全景图还需要加上 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',
] }
/>
我们可以使用一张覆盖周围环境的图像,而不是使用 6 张图像。
它就像一张 360 度全景照片,通常处于高动态范围,以使照明数据更准确。这是有道理的,因为光不会真正停留在某个范围内。如果你看太阳,它会比看灯泡灯亮得多(不要看太阳)。
the_sky_is_on_fire_2k.hdr
<Environment
background
files="./environmentMaps/the_sky_is_on_fire_2k.hdr"
/>
HDRIS 下载网址:
https://polyhaven.com/hdris
比下载这些 HDRI 更好的是,drei 创建了直接从 Poly Haven 获取文件的预设。
替换 files 为 preset
<Environment
background
preset="sunset"
/>
https://github.com/pmndrs/drei/blob/master/src/helpers/environment-assets.ts
假设我们希望在一侧有某种大的红色矩形,以确保有红光从这一侧照亮我们的物体。
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>
这样就有红色的光映射到其他物体上了
删除了预设更加明显:
默认情况下,环境映射的背景是黑色的,这就是为什么只照亮红色的一面。
我们可以通过在场景中渲染的场景上设置背景颜色来改变这一点
<Environment
background
>
<color args={ [ 'blue' ] } attach="background" />
{/* ... */}
</Environment>
当使用环境贴图作为背景时,我们有一种物体漂浮的感觉,因为图像是无限远的。
通过添加 ground 属性,环境贴图的投影将使对象下方的地板看起来好像很近。
使用以下参数 background 而不是属性添加 ground 属性:
<Environment
preset="sunset"
ground={ {
height: 7,
radius: 28,
scale: 100
} }
>
{/* ... */}
</Environment>
该地面被视为位于场景 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>
我们实际上可以删除或注释绿色地板平面,并使用 其 position
属性向上移动:
<ContactShadows
position={ [ 0, 0, 0 ] }
scale={ 10 }
resolution={ 512 }
far={ 5 }
color={ color }
opacity={ opacity }
blur={ blur }
/>
通过控件调整
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 }
})
<Environment
preset="sunset"
ground={ {
height: envMapHeight,
radius: envMapRadius,
scale: envMapScale
} }
>
</Environment>
我们做了很多配置。虽然它不是太复杂,但有时,我们只想要一个默认的好看的设置和最少的配置。
这就是 Stage 帮助者所做的。
Stage 将为我们设置环境地图、阴影和两个方向光。它还将场景居中。如果您想要快速简便的设置,这是一个很好的解决方案。
为了使事情变得简单并且不丢失我们之前的设置,我们将注释 JSX 中除
和
(甚至 )
之外的所有内容。
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/*
*/}
{/*
*/}
{/*
*/}
{/*
*/}
{/* */}
导入 Stage 自 @react-three/drei :
import { Stage, Environment, Sky, ContactShadows, RandomizedLight, AccumulativeShadows, softShadows, BakeShadows, useHelper, OrbitControls } from '@react-three/drei'
现在将两者 包装起来 :
我们仍然可以使用以下 contactShadows 属性来控制接触阴影:
<Stage
contactShadow={ { opacity: 0.2, blur: 3 } }
>
选择不同的环境贴图预设:
<Stage
contactShadow={ { opacity: 0.2, blur: 3 } }
environment="sunset"
>
更改方向灯 preset ( ‘rembrandt’ , ‘portrait’ , ‘upfront’ , ): ‘soft’
<Stage
contactShadow={ { opacity: 0.2, blur: 3 } }
environment="sunset"
preset="portrait"
>
更改照明强度:
<Stage
contactShadow={ { opacity: 0.2, blur: 3 } }
environment="sunset"
preset="portrait"
intensity={ 2 }
>
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>
</>
}
R3F 提供了一个名为 useLoader abstract loading 的钩子。
import { useLoader } from '@react-three/fiber'
要使用它,我们需要向它发送我们想要使用的Three.js加载程序类和文件的路径。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
export default function Experience()
{
const model = useLoader(GLTFLoader, './hamburger.glb')
console.log(model)
// ...
}
在场景中放置,我们需要的是 是某种我们想要放入其中的容器。
它不是我们在场景中能够看到的真实对象,但它是 R3F 支持的容器,它将处理和显示我们在其属性中 object 放置的任何内容。
export default function Experience()
{
const model = useLoader(GLTFLoader, './hamburger.glb')
return <>
{/* ... */}
<primitive object={ model.scene } scale={ 0.35 } />
</>
}
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
const model = useLoader(
GLTFLoader,
'./hamburger-draco.glb',
(loader) =>
{
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
loader.setDRACOLoader(dracoLoader)
}
)
加载大模型时推荐使用延时加载
要实现延迟加载,我们可以使用标签
是一个 React 组件,它将等待过程完成(在我们的例子中加载模型)然后再渲染组件。
这样就需要多创建一个组件来渲染加载中的显示:
我们需要将模型放在一个单独的组件中。
export default function Model()
{
return null
}
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 } />
}
在 Experience.jsx 中,导入 Suspense 自 react :
import { Suspense } from 'react'
这样就实现了加载完成后再渲染该组件了
Fallback 是用户在组件未准备就绪时(在我们的例子中,在模型加载时)将看到的内容。
要定义回退,我们可以使用以下 fallback 属性:
<Suspense
fallback={ }
>
我们可以设置一个 :
}
>
它已经更好了,但让我们更进一步,创建一个组件。
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>
}
<Suspense fallback={ <Placeholder /> }>
多亏了 drei,还有一种更简单的方法。
Drei 实现了多个加载器帮助程序,如 useGLTF
和 useFBX
。
在 Model.jsx 中,导入 useGLTF 自 @react-three/drei :
import { useGLTF } from '@react-three/drei'
现在,将整个 useLoader() 调用 useGLTF() 替换为文件的路径作为唯一参数:
export default function Model()
{
const model = useGLTF('./hamburger.glb')
// ...
}
就是这样,我们可以删除 useLoader 、 GLTFLoader 和 DRACOLoader 的导入。对 dracoLoader 也不需要了
const model = useGLTF('./hamburger-draco.glb')
目前,我们的模型只有在组件实例化时才会开始加载。
我们可以使用 上useGLTF 的 preload方法做到这一点。
在 Model.jsx 文件中,在 Model 函数之后,使用模型 URL 调用: preload
export default function Hamburger({ ...props })
{
// ...
}
useGLTF.preload('./hamburger-draco.glb')
在我们的例子中,如果我们想要第二个汉堡包怎么办?还是三个?还是一百个?
Drei 通过 Clone 助手使这成为可能。
在 Model.jsx 中 @react-three/drei :
import { Clone, useGLTF } from '@react-three/drei'
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 } />
</>
}
如果检查性能监视,则会发现几何图形和着色器的数量保持不变。 Clone 创建多个网格,它仍然基于相同的几何形状和材料。
如果我们想操纵汉堡包的不同部分,我们需要遍历加载的模型,寻找合适的子项,以某种方式保存它,并应用我们需要的任何东西。
另一种选择是在 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/
把代码拷贝使用:
/*
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");
这个组件几乎可以使用了,但我们需要做一些重构。
// ...
export default function Hamburger(props) {
// ...
import Hamburger from './Hamburger.jsx'
export default function Experience()
{
return <>
{/* ... */}
<Suspense fallback={ <Placeholder position-y={ 0.5 } scale={ [ 2, 3, 2 ] } /> }>
<Hamburger scale={ 0.35 } />
</Suspense>
</>
}
由于模型的每个部分都写在 中 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>
);
}
阴影看起来有点奇怪,条纹穿过汉堡包的表面。
这被称为阴影痤疮,这是由于模型在自身上投射阴影。
我们可以通过调整 bias or shadowBias 中的定向光影来解决这个问题 Experience.jsx :
<directionalLight castShadow position={ [ 1, 2, 3 ] } intensity={ 1.5 } shadow-normalBias={ 0.04 } />
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 }
/>
}
import Fox from './Fox.jsx'
export default function Experience()
{
return <>
{/* ... */}
<Fox />
</>
}
import { useAnimations, useGLTF } from '@react-three/drei'
export default function Fox()
{
const fox = useGLTF('./Fox/glTF/Fox.gltf')
console.log(fox)
// ...
}
export default function Fox()
{
const fox = useGLTF('./Fox/glTF/Fox.gltf')
const animations = useAnimations(fox.animations, fox.scene)
console.log(animations)
// ...
}
现在,我们可以访问模型提供的各种动画,并且每个动画都已使用动画的名称( Run , SurveyWalk 如果是 Fox)转换为 AnimationAction,并且这些操作在 animation.actions 对象中可用。
但是在开始任何这些操作之前,最好在组件首次完成渲染后执行此操作,我们可以使用 useEffect .
import { useEffect } from 'react'
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()
}, [])
// ...
}
这样就可以动了
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)
}, [])
使用 leva 控制
import { useControls } from 'leva'
在 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()
}, [])
// ...
}
目前, useEffect dependencies 数组为空,这意味着该函数在第一次渲染后只会被调用一次。这正是设置特定依赖项的用武之地。我们希望在第一次渲染时调用该函数,但也希望在 animationName 更改时调用该函数。
将 animationName 添加到 dependencies 数组:
useEffect(() =>
{
// ...
}, [ animationName ])
当我们更改 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 ])
return 中销毁:
useEffect(() =>
{
const action = animations.actions[animationName]
action.fadeIn(0.5).play()
return () =>
{
action.fadeOut(0.5)
}
}, [ animationName ])
或者:
useEffect(() =>
{
const action = animations.actions[animationName]
action
.reset()
.fadeIn(0.5)
.play()
// ...
}, [ animationName ])
import { Text3D, OrbitControls } from '@react-three/drei'
下载字体:
如果需要,您可以使用此网站创建自己的字体 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>
</>
}
import { Center, Text3D, OrbitControls } from '@react-three/drei'
HELLO R3F
配置参数,好看点:
HELLO R3F
首先,我们需要加载 matcap 纹理
我们将使用一个名为 useMatcapTexture drei 的助手,它将自动从此存储库加载纹理https://github.com/emmelleppi/matcaps.
import { useMatcapTexture, Center, Text3D, OrbitControls } from '@react-three/drei'
因为需要给模型设置不同的材质,所以不直接将整个模型添加到场景
const { nodes } = useGLTF('./model/portal.glb')
console.log(nodes)
添加 geometry:
export default function Experience()
{
// ...
return <>
{/* ... */}
<mesh geometry={ nodes.baked.geometry } />
</>
}
加载这个模型的纹理进行贴图
import { useTexture, useGLTF, OrbitControls } from '@react-three/drei'
export default function Experience()
{
// ...
const bakedTexture = useTexture('./model/baked.jpg')
bakedTexture.flipY = false
console.log(bakedTexture)
// ...
}
定个心:
import portalVertexShader from './shaders/portal/vertex.glsl'
import portalFragmentShader from './shaders/portal/fragment.glsl'
console.log(portalFragmentShader)
传递变量;
,drei 提供了一个名为 shaderMaterial helper 的帮助程序,用于创建一个 ShaderMaterial,然后我们将在 JSX 中提供该 ShaderMaterial,简化了数据传递过程
import { shaderMaterial, Sparkles, Center, useTexture, useGLTF, OrbitControls } from '@react-three/drei'
然后,在 Experience 函数之前调用它,并在 PortalMaterial 变量中分配结果:
const PortalMaterial = shaderMaterial(
{
uTime: 0,
uColorStart: new THREE.Color('#ffffff'),
uColorEnd: new THREE.Color('#000000')
},
portalVertexShader,
portalFragmentShader
)
export default function Experience()
{
// ...
}
现在,为了将它转换为我们可以在 JSX 中使用的 R3F 标签,我们将使用 extend
import { extend } from '@react-three/fiber'
const PortalMaterial = shaderMaterial(
// ...
)
extend({ PortalMaterial })
更新 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 } />
)
}
在原生 threejs 中使用点击事件需要使用光线投射来实现,而这里并不需要
我们需要做的就是为场景中的对象添加一个 onClick 属性(如 ),并为其提供一个函数
export default function Experience()
{
// ...
const eventHandler = () =>
{
console.log('the event occured')
}
// ...
}
点击改变颜色使用 ref 引用
const eventHandler = () =>
{
cube.current.material.color.set(`hsl(${Math.random() * 360}, 100%, 75%)`)
}
打印事件参数:
const eventHandler = (event) =>
{
console.log(event)
cube.current.material.color.set(`hsl(${Math.random() * 360}, 100%, 75%)`)
}
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
除了 onClick 还有:
onContextMenu 在上下文菜单应出现时触发。
onDoubleClick:双击事件
onPointerUp:鼠标松开
onPointerDown
onPointerOver /onPointerEnter:当光标或手指刚好位于对象上方时,将触发该事件。
onPointerOut 和 onPointerLeave:
onPointerMove
onPointerMissed
当出现物体被前面物体遮挡了点击仍然有效时可以设置:
<mesh position-x={ - 2 } onClick={ (event) => event.stopPropagation() }>
{/* ... */}
</mesh>
监听鼠标进入和离开三维对象:
{ } }
onPointerLeave={ () => { } }
>
改变光标的样式:
{ document.body.style.cursor = 'pointer' } }
onPointerLeave={ () => { document.body.style.cursor = 'default' } }
>
加载个多结构的对象:
export default function Experience()
{
// ...
const hamburger = useGLTF('./hamburger.glb')
return <>
{/* ... */}
>
}
由于 是 object 的简单占位符,我们可以像听任何其他对象一样监听它上面的事件
{
console.log('click')
} }
/
每次点击最多可以得到 4 个 console
这是因为光线同时穿过多个物体。
即使我们在父母身上听事件,R3F 实际上也会测试孩子,这是有充分理由的,因为没有孩子对象,父母什么都不是
我们实际上可以使用以下 object 属性来测试哪些对象触发了它 event :
<primitive
object={ hamburger.scene }
scale={ 0.25 }
position-y={ 0.5 }
onClick={ (event) =>
{
console.log(event.object)
} }
/>
我们可以使用 stopPropagation 方法来停止event传播 :
<primitive
object={ hamburger.scene }
scale={ 0.25 }
position-y={ 0.5 }
onClick={ (event) =>
{
console.log(event.object)
event.stopPropagation()
} }
/>
这样打印的就只有一个了
点击事件对 CPU 来说是一项相当繁重的任务。
尽量减少侦听事件的对象数量,并避免测试复杂的几何图形。如果你在互动时发现冻结,即使是短暂的冻结,你也会有更多的优化工作要做。
我们可以应用的一个简单的优化是 drei 的 meshBounds 助手。
此帮助程序将在网格周围创建一个理论球体(称为边界球体),并且指针事件将在该球体上进行测试,而不是测试网格的几何形状
如果您不需要对复杂的几何形状进行非常精确的检测,这将非常有用(用来替代判断)
import { meshBounds, useGLTF, OrbitControls } from '@react-three/drei'
<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>
如果您有非常复杂的几何图形,并且仍然需要指针事件准确,则还可以使用 BVH(边界体积层次结构)
这是一种更复杂的方法,但有了 drei 的 useBVH 助手,它变得容易了。
后处理也受益于 React 和 R3F 系统,因为它更容易实现,但在某些方面也得到了优化
在原生 threejs 中我们通过后处理通道来实现后处理效果,乒乓缓冲
import { EffectComposer } from '@react-three/postprocessing'
虽然它与 EffectComposer 我们在原生Three.js中使用的名称相同,但它不是同一个类。
export default function Experience()
{
return <>
{/* ... */}
>
}
默认情况下,它的值为 at 8 ,我们可以将其降低到 0 以完全禁用它。
<EffectComposer multisampling={ 0 }>
</EffectComposer>
禁用多重采样时,性能应该会更好
Post Processing
React-postprocessing
import { Vignette, EffectComposer } from '@react-three/postprocessing'
<EffectComposer>
<Vignette
offset={ 0.3 }
darkness={ 0.9 }
/>
</EffectComposer>
blendFunction 工作方式有点像您可以在图像编辑软件(如 Photoshop)中找到的混合。这就是我们所画的颜色与后面的颜色融合的方式。
。。。。