首页 > 其他分享 >threejs中渲染html

threejs中渲染html

时间:2023-03-30 18:47:33浏览次数:43  
标签:threejs const 渲染 react html position import rotation

背景

最近中看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

相关文章

  • HTML元素截图(html2canvas)
    html2canvas官网 :http://html2canvas.hertzen.com/开源地址:https://github.com/niklasvh/html2canvas <body><divid="div"><span>内容</span><divstyle="height:300px;border:solid1pxred;width:100px">......
  • 移动网站增加HTML5特性流量可提高28%
    移动科技平台Usablenet的最新数据显示:在网站中加入互动元素可极大提高网站流量。如果零售商在平台中采用具有HTML5特性,比如互动画册,地理位置服务,可浮动可扩展的模块,其网站......
  • 搭上HTML5的快车,磊友获千万美元融资
    风险投资公司美国中经合集团今日宣布领投北京磊友信息科技有限公司。磊友专注于HTML5游戏在移动设备上的研发和运营,本轮投资由中经合和创新工场发展基金共同投资。磊友......
  • threejs-camera&controls&renderer(WebGLRenderer)
    ArrayCamera:一般用于,展示益、一个场景存在多个物体,每个物体各自拥有自己的视角的这种场景。CubeCamera:一次性创建六个方位的相机(类似于正方体六个面,立方全景图中所有方......
  • blender建模渲染Tips
    blender渲染灯光的三种方式1,常规灯光:shift+A选择灯光。2,世界环境光:右侧地球图标调整。3,物体自发光:把渲染物体变成一个发光体来进行调节灯光。渲染视窗的调节ctrl+......
  • HTML的由来
    超文本标记语言(HTML)起源于标准通用标记语言(SGML),由世界上最大的粒子物理研究实验室欧洲核子研究中心CERN(theEuropeanOrganizationforNuclearResearch)于1991年首......
  • BI报告:App主导现在 HTML5领衔未来
    HTML5能够让开发人员构建丰富的基于Web应用程序,使其能在任何设备中使用标准的Web浏览器。很多人认为HTML5将会让App过时。到底App还是HTML5会是谁赢得最后的胜利,在业界也有......
  • echarts export three types picture: png、html、svg
    import'./styles.css'importechartsfrom'echarts'import{saveAs}from'file-saver'importJSPDFfrom'jspdf'import{init}from'canvas-to-blob'init(......
  • 若依框架-Vue实用框架(权限控制和页面渲染)(四)
    Vue实用框架(权限控制和页面渲染)路由的组成前端token获取那一步中有一块内容,只是简单提了一下,但其实实际涉及到的内容很多: 用户信息的获取第一步的GetInfo后端接口不......
  • 第九篇 vue - 基础 - 列表渲染
    v-for我们可以使用v-for指令基于一个数组来渲染一个列表。v-for指令的值需要使用iteminitems形式的特殊语法,其中items是源数据的数组,而item是迭代项的别名dat......