背景
最近中看threejs的时候发现一个好玩的事情,可以在threejs中渲染普通的html。threejs本身可以做各种炫酷的界面,但是与用户交互的时候写起来没有用dom实现方便,但是如果可以将已有的dom渲染到threejs中,那么就可以实现非常炫酷的界面,也能提高用户的体验。
依赖介绍
这里使用react框架,
状态管理库选用的recoil(这个库是由react官方开发的一个状态管理库,个人感觉比redux,mobx好用),
需要threejs自然还有three这个库,
用@react-three/fiber来结合threejs与react(这个库很好用,像react组件一样搭建threejs场景),
然后使用@react-three/drei提供的Html工具来实现html的渲染,
中间的一些过度动画使用gsap来实现。
总结需要安装的依赖如下
npm i recoil threejs gsap @react-three/fiber @react-three/drei
@react-three/drei提供的Html
这个工具本身就可以实现将html渲染到three中,具体使用如下
import { Canvas } from '@react-three/fiber'
import { Html } from '@react-three/drei'
import { Vector3, Euler } from 'three'
function () {
return (
<Canvas>
<Html
position={new Vector3(1,1,1)} // 表示这个dom显示到3d世界中的位置
rotation={new Euler(1,1,1)} // 表示这个dom在3d世界中旋转的角度
transform
>
<div>
这里写普通的react node即可
</div>
</Html>
</Canvas>
)
}
这里的缺陷与想要达到的效果
若场景中有多个html,都分布在不同的位置,想要实现从一个html到另一个html,只能手动调整摄像头位置,若用手动调整的话,很难找到一个合适的位置去看这个html。
想要实现一个调用去看某个html的方法,场景就自动切换到合适的位置(这个html正面中心的位置,距离html多远可以由传参的方式决定),没有看的html就不渲染
实现的思路
每次实例化一个html都会注册一个唯一的key,当调用某个方法传入这个key时即可自动转换到当前html的观看位置,通过recoil创建一个atom,用来保存注册的html的key,同时导出一个方法用于调用跳转html视角。
store的实现
代码如下,将其存在 store.ts文件中
import { useCallback } from 'react'
import { atom, useSetRecoilState, useRecoilValue } from 'recoil'
export type TriggerKey = string
type SeeHtmlFn = () => void
type RenderHtml = {
current?: TriggerKey, // 当前视角所在的html
seeFnMap: Partial<Record<TriggerKey, SeeHtmlFn>> //保存了所有注册的html,在跳转到某个html时触发
}
const seeHtml = atom<RenderHtml>({ // 实例化一个atom
key: 'seeHtml',
default: {
seeFnMap: {}
}
})
export function useAddSee() { // 注册一个看某个html的方法
const setSeeHtml = useSetRecoilState(seeHtml)
const addSee = useCallback((key: TriggerKey, fn: SeeHtmlFn) => {
setSeeHtml(oldValue => ({
current: oldValue.current,
seeFnMap: {
...oldValue.seeFnMap,
[key]: fn
}
}))
}, [])
return addSee
}
export function useSetRenderCurrent() { // 注册渲染html时标记为当前的key
const setSeeHtml = useSetRecoilState(seeHtml)
const setRenderCurrent = useCallback((key: TriggerKey) => {
setSeeHtml(oldValue => ({
...oldValue,
current: key
}))
}, [])
return setRenderCurrent
}
export function useRenderCurrent() { // 返回一个当前看的html的key
const seeHtmlValue = useRecoilValue(seeHtml)
return seeHtmlValue.current
}
export function useGoTo() { // 返回一个去到某个html观看视角的方法
const seeHtmlValue = useRecoilValue(seeHtml)
const goTo = useCallback((key: TriggerKey) => {
seeHtmlValue.seeFnMap[key]?.()
}, [seeHtmlValue])
return goTo
}
export default seeHtml
component的实现
import { useCallback, useEffect, useRef } from 'react'
import { Html } from '@react-three/drei'
import { HtmlProps } from '@react-three/drei/web/Html'
import { useThree } from '@react-three/fiber'
import { Vector3, Euler, Camera } from 'three'
import gsap from 'gsap'
import { useAddSee, TriggerKey, useRenderCurrent, useSetRenderCurrent } from './store'
type Props = {
triggerKey: TriggerKey,
style?: React.CSSProperties,
className?: string,
distance?: number,
} & HtmlProps
type RCamera = Camera & {
manual?: boolean | undefined;
}
function RenderHtml({
triggerKey,
style,
className,
children,
distance = 4, // 默认在距离html4个单位的地方观看
position = new Vector3(),
rotation = new Euler(),
...extra
}: Props) {
const camera = useThree(state => state.camera)
const prePosition = useRef<Vector3>()
const preRotation = useRef<Euler>()
const preCamera = useRef<RCamera>()
const preD = useRef<number>()
const addSee = useAddSee()
const renderCurrent = useRenderCurrent()
const setRenderCurrent = useSetRenderCurrent()
const resetCamera = useCallback((camera: RCamera, position: Vector3, rotation: Euler, offset: Vector3) => { // 重置相机到最佳观看位置
gsap.to(camera.position, {
x: position.x + offset.x,
y: position.y + offset.y,
z: position.z + offset.z
})
gsap.to(camera.rotation, {
x: rotation.x,
y: rotation.y,
z: rotation.z
})
}, [])
useEffect(() => {
const _position = position as Vector3
const _rotation = rotation as Euler
if (preCamera.current === camera && preD.current === distance &&
prePosition.current && _position.equals(prePosition.current) &&
preRotation.current && _rotation.equals(preRotation.current)) {
return // 若位置,旋转角度,距离没有变化则不重新设置相机位置
}
preCamera.current = camera
preD.current = distance
prePosition.current = _position
preRotation.current = _rotation
const offset = new Vector3(0, 0, distance)
offset.applyEuler(_rotation)
addSee(triggerKey, () => {
setRenderCurrent(triggerKey)
resetCamera(camera, _position, _rotation, offset)
})
}, [camera, position, rotation, distance])
useEffect(() => {
if (renderCurrent !== triggerKey) return
const _position = position as Vector3
const _rotation = rotation as Euler
const offset = new Vector3(0, 0, distance)
offset.applyEuler(_rotation) // 将距离转换到平面的法线方向上
resetCamera(camera, _position, _rotation, offset)
}, [camera, position, rotation, distance, renderCurrent])
if (renderCurrent !== triggerKey) { // 若当前渲染的html不是当前的这个则不渲染
return <></>
}
return (
<Html
position={position}
rotation={rotation}
transform
{...extra}
>
<div style={style} className={className}>
{children}
</div>
</Html>
)
}
export default RenderHtml
最后使用
import { useState, useCallback, useEffect } from 'react'
import { Vector3, Euler } from 'three'
import { useGoTo } from './renderHtml/store'
import RenderHtml from './renderHtml'
function RenderHtmlTest() {
const [count, setCount] = useState(4)
const goTo = useGoTo()
useEffect(() => {
document.addEventListener('click', () => {
goTo('test')
})
}, [goTo])
return (
<RenderHtml
triggerKey='test'
distance={count}
position={new Vector3(6, -1, 2)}
rotation={new Euler(0, -Math.PI * 90 / 180, 0)}
>
<input />
<p style={{ margin: '8px 0', backgroundColor: '#ccc', padding: '4px 8px', borderRadius: '4px' }}>摄像头距离: {count}</p>
<button onClick={() => setCount(count + 1)}>摄像头距离+1</button>
<br />
<button style={{ marginTop: 8 }} onClick={() => setCount(count - 1)}>摄像头距离-1</button>
</RenderHtml>
)
}
总结
threejs结合html可以实现非常炫酷的页面,改变当前页面单调的访问方式。以上实现了快速切换到某个html,相当于切换路由了。
标签:threejs,const,渲染,react,html,position,import,rotation From: https://www.cnblogs.com/mbbk/p/threejs_render_html.html