能否在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
相关产品推荐
相关产品推荐

