react-three-fiber中超canvas的HTML覆盖层裁剪问题求解
问题根因
垂直裁剪不是Canvas层级限制导致,是@react-three/drei的Html组件默认配置+代码逻辑错误共同导致:
- 直接把
<Html>放在<instancedMesh>根节点下,未绑定悬停实例的实际位置,Html默认挂载在WebGL渲染器生成的overflow: hidden容器内,超出变换区域的内容会被直接裁剪 - 未做悬停状态的条件渲染,给全部15个实例都渲染了同一个Tooltip,多个重叠Html元素互相干扰
- Html组件未配置溢出自适应规则,长内容没有自动滚动/自适应逻辑,高度设置受容器变换约束不生效
最佳实现方案
核心是通过drei Html组件自带的配置脱离裁剪容器,同时只在悬停对应实例时渲染Tooltip,绑定到对应实例的世界坐标上,具体配置要点:
- 给Html组件加
portal属性,直接把Tooltip挂载到document.body或者Canvas外层DOM节点,彻底脱离Canvas内部的裁剪容器 - 给Html组件加
occlude属性,保证被3D物体遮挡时自动隐藏,避免穿模 - 不要把Html直接放在instancedMesh根节点,悬停时动态计算当前hover实例的世界坐标,把Html定位到对应坐标位置
- 给Tooltip容器加最大高度、溢出滚动样式,长内容自动出滚动条不会被裁剪
具体代码修改
1. 修改IScatter.jsx
核心改动:悬停时动态获取实例的世界坐标,只渲染单个Tooltip,配置Html的portal属性挂载到外层
import * as THREE from "three"; import React, { useRef, useState, useMemo } from "react"; import { Html } from "@react-three/drei"; import data from "./story.json"; import Tooltip from "./Tooltip"; const points = [ [1, 0, -1], [0, 1, -0.5], [0.5, 0.5, 0.5], [1,0.25,-1], [1,0,1], [0,1,0.5] ]; const colors = [0,0,0,5,5,5]; const tempColor = new THREE.Color(); const tempSphere = new THREE.Object3D(); const dummyObj = new THREE.Object3D(); const Spheres = () => { const material = new THREE.MeshLambertMaterial({ opacity: 0.5, side: THREE.DoubleSide, transparent: true,}); const spheresGeometry = new THREE.SphereGeometry(0.25, 15, 15); const ref = useRef(); const [hoveredPos, setHoveredPos] = useState(null); // 初始化实例矩阵 useMemo(() => { points.forEach((val, row) => { tempSphere.position.set(val[0], val[1], val[2]); tempSphere.updateMatrix(); ref.current?.setMatrixAt(row, tempSphere.matrix); ref.current?.setColorAt(row, new THREE.Color(`hsl(${colors[row]*100}, 100%, 50%)`)); }); if(ref.current) { ref.current.instanceMatrix.needsUpdate = true; ref.current.instanceColor.needsUpdate = true; } }, []) // 悬停时获取当前实例的世界坐标 const handlePointerOver = (e) => { e.stopPropagation(); const instanceId = e.instanceId; // 读取对应实例的位置 ref.current.getMatrixAt(instanceId, dummyObj.matrix); dummyObj.matrix.decompose(dummyObj.position, dummyObj.quaternion, dummyObj.scale); setHoveredPos(dummyObj.position.clone()); // 高亮当前悬停实例 ref.current.setColorAt(instanceId, new THREE.Color("hsl(43, 100%, 50%)")); ref.current.instanceColor.needsUpdate = true; } const handlePointerOut = () => { // 重置所有实例颜色 points.forEach((val, row) => { ref.current.setColorAt(row, new THREE.Color(`hsl(${colors[row]*100}, 100%, 50%)`)); }); ref.current.instanceColor.needsUpdate = true; setHoveredPos(null); } return ( <> <instancedMesh onPointerOver={handlePointerOver} onPointerOut={handlePointerOut} ref={ref} rotation={[0,30,0]} args={[spheresGeometry, material, 15]} /> {/* 仅悬停时渲染Tooltip,挂载到body脱离Canvas裁剪 */} {hoveredPos && ( <Html position={hoveredPos.toArray()} distanceFactor={5} portal={document.body} zIndexRange={[100, 0]} occlude > <Tooltip title={data.title} story={data.story} author={data.author} /> </Html> )} </> ); }; export default Spheres;
2. 修改Tooltip.jsx
添加自适应样式,长内容自动滚动,避免溢出裁剪
import React from "react"; function Tooltip( {title, story, author} ){ return ( <div style={{ display: "inline-block", width: "min(500px, 50vw)", maxHeight: "40vh", padding: "16px", background: "white", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.15)", overflowY: "auto", pointerEvents: "none" }}> <h6 className="title is-6" style={{margin: "0 0 8px 0", fontSize: "16px"}}>{ title }</h6> <p style={{margin: "0 0 8px 0", lineHeight: "1.5", fontSize: "14px"}}>{story}</p> <span className="is-pulled-right" style={{fontSize: "12px", color: "#666"}}>-{author}</span> </div> ) } export default Tooltip;
3. 可选优化
如果不想把Html挂载到body,也可以给Canvas外层的div加position: relative,把portal属性指向这个外层div,方便统一管理弹窗层级。
效果说明
修改后Tooltip直接渲染在页面顶层容器,不会被Canvas的overflow规则裁剪:
- 宽度自动适配屏幕,最大500px不会超出移动端视口
- 高度最大占屏幕40%,内容过长时自动出现垂直滚动条
- 仅鼠标悬停到对应球体实例时才会显示Tooltip,跟随3D场景的缩放、旋转自动调整位置
- 被其他3D物体遮挡时Tooltip会自动隐藏,不会出现穿模问题
较新版本的@react-three/drei已经修复了Html组件内部容器的overflow裁剪问题,但挂载到外层DOM节点的方案兼容性最好,不会受Canvas尺寸、变换的影响。
内容的提问来源于stack exchange,提问作者jar
相关产品推荐
相关产品推荐

