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

react-three-fiber中超canvas的HTML覆盖层裁剪问题求解

问题根因

垂直裁剪不是Canvas层级限制导致,是@react-three/drei的Html组件默认配置+代码逻辑错误共同导致:

  1. 直接把<Html>放在<instancedMesh>根节点下,未绑定悬停实例的实际位置,Html默认挂载在WebGL渲染器生成的overflow: hidden容器内,超出变换区域的内容会被直接裁剪
  2. 未做悬停状态的条件渲染,给全部15个实例都渲染了同一个Tooltip,多个重叠Html元素互相干扰
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 23:33:26