You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否在React Three.js的Decal组件中嵌入HTML?

问题原因

<Html>组件和<Text>组件的工作逻辑完全不同:

  • <Text>是基于Three.js的3D文本几何体,属于WebGL渲染管线的一部分,能被RenderTexture捕获并渲染成纹理,最终贴在Decal上。
  • <Html>是把真实DOM元素挂载到Canvas外部,通过CSS变换模拟3D空间位置,不在WebGL渲染流程里,所以放在RenderTexture内部不会被渲染到纹理,反而会脱离3D场景透视规则,出现定位错误。
解决方案

要在Decal上显示HTML内容,得先把HTML转换成WebGL可识别的纹理,再将纹理应用到Decal上。以下是两种可行实现方式:

方法1:用html2canvas将HTML转为纹理

步骤1:安装依赖

npm install html2canvas

步骤2:修改组件代码

import { useRef, useState, useEffect } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { suspend } from 'suspend-react'
import {
  useGLTF,
  useCursor,
  useTexture,
  Text,
  Decal,
  Environment,
  OrbitControls,
  RenderTexture,
  RandomizedLight,
  PerspectiveCamera,
  AccumulativeShadows,
} from '@react-three/drei'
import html2canvas from 'html2canvas'
import * as THREE from 'three'

const bunny = import('@pmndrs/assets/models/bunny.glb')

export const App = () => (
  <Canvas shadows camera={{ position: [-5, 5, 10], fov: 15 }}>
    <ambientLight intensity={0.25} />
    <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
    <pointLight position={[-10, -5, -10]} />
    <group position={[0, -0.75, 0]}>
      <Bun position={[0, 0.9, 0]} />
      <AccumulativeShadows frames={80} color="black" opacity={1} scale={12} position={[0, 0.04, 0]}>
        <RandomizedLight amount={8} radius={5} ambient={0.5} position={[5, 5, -10]} bias={0.001} />
      </AccumulativeShadows>
    </group>
    <Environment preset="city" resolution={512} />
    <OrbitControls makeDefault />
    {/* 隐藏的HTML元素,用于生成纹理 */}
    <div style={{ position: 'absolute', top: -9999, left: -9999 }} ref={htmlRef}>
      <h1 style={{ color: 'orange', fontSize: '40px', margin: 0 }}>hello from HTML</h1>
    </div>
  </Canvas>
)

function Bun(props) {
  const textRef = useRef()
  const htmlRef = useRef()
  const [htmlTexture, setHtmlTexture] = useState(null)
  const [pmndrs, react, three] = useTexture(['/pmndrs.png', '/react.png', '/three.png'])
  const { nodes } = useGLTF(suspend(bunny).default)

  // 生成HTML纹理
  useEffect(() => {
    const generateTexture = async () => {
      const canvas = await html2canvas(htmlRef.current)
      const texture = new THREE.Texture(canvas)
      texture.needsUpdate = true
      setHtmlTexture(texture)
    }
    generateTexture()
  }, [])

  useFrame((state) => (textRef.current.position.x = Math.sin(state.clock.elapsedTime) * 5.5))
  return (
    <mesh castShadow receiveShadow geometry={nodes.mesh.geometry} {...props} dispose={null}>
      <meshStandardMaterial color="black" roughness={0} metalness={0.5} />
      {/* 原有的RenderTexture贴花,新增HTML纹理的平面 */}
      <Decal position={[0, 0, 0.75]} rotation={[-0.4, Math.PI, 0]} scale={[0.9, 0.25, 1]}>
        <meshStandardMaterial roughness={0.6} transparent polygonOffset polygonOffsetFactor={-10}>
          <RenderTexture attach="map" anisotropy={16}>
            <PerspectiveCamera makeDefault manual aspect={0.9 / 0.25} position={[0, 0, 5]} />
            <color attach="background" args={['#af2040']} />
            <ambientLight intensity={0.5} />
            <directionalLight position={[10, 10, 5]} />
            <Text rotation={[0, Math.PI, 0]} ref={textRef} fontSize={4} color="white">
              hello from drei
            </Text>
            {/* 添加HTML纹理的平面 */}
            {htmlTexture && (
              <mesh position={[0, -2, 0]} rotation={[0, Math.PI, 0]} scale={[4, 1, 1]}>
                <planeGeometry />
                <meshBasicMaterial map={htmlTexture} transparent />
              </mesh>
            )}
            <Dodecahedron />
          </RenderTexture>
        </meshStandardMaterial>
      </Decal>
      {/* 单独用Decal显示HTML内容(可选) */}
      {htmlTexture && (
        <Decal position={[0, -0.3, 0.75]} rotation={[-0.4, Math.PI, 0]} scale={[0.9, 0.25, 1]}>
          <meshStandardMaterial roughness={0.6} transparent polygonOffset polygonOffsetFactor={-10} map={htmlTexture} />
        </Decal>
      )}
      <Decal position={[-0.7, 0.55, 0.3]} rotation={[1, 0, Math.PI]} scale={0.2} map={pmndrs} map-anisotropy={16} />
      <Decal position={[0.2, 0.2, 0.2]} rotation={0} scale={0.25} map={react} map-anisotropy={16} />
    </mesh>
  )
}

// Dodecahedron组件保持不变
function Dodecahedron(props) {
  const meshRef = useRef()
  const texture = useTexture('/react.png')
  const [hovered, hover] = useState(false)
  const [clicked, click] = useState(false)
  useCursor(hovered)
  useFrame((state, delta) => (meshRef.current.rotation.x = meshRef.current.rotation.y += delta))
  return (
    <mesh
      {...props}
      ref={meshRef}
      scale={clicked ? 2.25 : 1.75}
      onClick={() => click(!clicked)}
      onPointerOver={() => hover(true)}
      onPointerOut={() => hover(false)}>
      <dodecahedronGeometry args={[0.75]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'goldenrod'} />
      <Decal position={[0, -0.2, 0.5]} scale={0.75} map={texture} map-anisotropy={16} />
    </mesh>
  )
}

方法2:用<Html>组件同步Decal定位

如果不需要将HTML渲染到3D纹理,只是想让HTML内容跟随Decal位置,可直接将<Html>放在Canvas内部,同步Decal的变换参数:

// 在Bun组件的mesh内部添加
<Html position={[0, 0, 0.75]} rotation={[-0.4, Math.PI, 0]} scale={0.01}>
  <h1 style={{ color: 'orange', margin: 0 }}>hello from HTML</h1>
</Html>

注意:<Html>的缩放单位与3D场景不同,需根据实际效果微调scale值。

关键注意点
  • 禁止将<Html>放在RenderTexture内部,它不属于WebGL渲染管线,无法被捕获为纹理。
  • 使用html2canvas时,部分复杂CSS属性可能无法被正确转换,需测试兼容性。
  • 若HTML内容动态更新,需在内容变化后重新生成纹理并设置texture.needsUpdate = true。

内容的提问来源于stack exchange,提问作者psygo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 11:45:00