在3d-force-graph中使用.nodeThreeObject将多个节点的显示合并在一起

前端开发 2026-07-09

我正在研究Vasturiano的(令人震撼、如同被魔法迷住的)3d-force-graph,在把三个看起来非常简单的元素:一个sprite、一个图片,以及图片上的文本,组合在一起、再分离开来时遇到了困难。

除了可视化中表示的“学者”之外,我还想用图片来表示“关键词”(使用./hashtag.png)和“学科领域”(使用./discipline.png)。最终的情况是,关键词(./hashtag.png)和学科(./discipline.png)会与精灵图片叠加在一起,导致没有显示任何文本标签:#keyword被错误地与默认节点精灵叠加,标签缺失。

  1. 我希望每个学者使用默认的精灵(彩色球)来显示,在其上方放置 node.color 文本。
  2. 我希望每个关键词使用 /imgs/hashtag.png 图像来显示,在其上方放置 node.color 文本。
  3. 我希望每个学科领域使用 /imgs/disipline.png 显示,在其上方放置 node.color 文本。

使用PNG也意味着我不能像把它们作为精灵那样给它们着色 node.color,这点我也想修正。我应该创建一组新的精灵(如果要,应该怎么做?),还是修改现有代码来分离并标注这三种不同的组?

这是完整的可视化,但请记住我只是在测试它,.json文件条目已手动编辑以包含 typehttps://greg-gan.github.io/JTC_Oak_visualization_trial/public/problems.html

以下是代码。它应该作为一个模块读取,因此在片段中可能无法运行。

import SpriteText from "https://esm.sh/three-spritetext";
import * as THREE from "https://esm.sh/three";
import * as _ from "https://cdn.jsdelivr.net/npm/3d-force-graph";

function main(data) { // introduced for readability, so the data is defined at the end
  const elem = document.getElementById('3d-graph');
  const img = document.getElementById('hashtag');
    img.src = "https://upload.wikimedia.org/wikipedia/commons/a/a0/Slightlysmilingface.svg";
  const img = document.getElementById('discipline');
    img.src = "https://upload.wikimedia.org/wikipedia/commons/f/f8/Cyan_gradient_grimacing_face_emoji.svg";
  const Graph = new ForceGraph3D(elem)
    .graphData(data)
    .nodeLabel(node => `${node.user}: ${node.description}`)
    .nodeThreeObjectExtend(true)
    .onNodeClick(node => {
      // Aim at node from outside it
      const distance = 40;
      const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);
      const newPos = node.x || node.y || node.z ?
        {
          x: node.x * distRatio,
          y: node.y * distRatio,
          z: node.z * distRatio
        } :
        {
          x: 0,
          y: 0,
          z: distance
        }; // special case if node is in (0,0,0)

      Graph.cameraPosition(
        newPos, // new position
        node, // lookAt ({ x, y, z })
        3000 // ms transition duration
      );
    })

    .nodeAutoColorBy('group')
    .nodeLabel('text') //scholar or #keyword or discipline
    .nodeThreeObject(node => {
      if (node.type == "keywords") {
        const imgTexture = new THREE.TextureLoader().load('hashtag');
        imgTexture.colorSpace = THREE.SRGBColorSpace;
        const material = new THREE.SpriteMaterial({
          map: imgTexture
        });
        const sprite = new THREE.Sprite(material);
        sprite.scale.set(14, 14);
        console.log(sprite);
        return sprite;
      } else if (node.type == "scholar") {
        const imgTexture = new THREE.TextureLoader().load('discipline');
        imgTexture.colorSpace = THREE.SRGBColorSpace;
        const material = new THREE.SpriteMaterial({
          map: imgTexture
        });
        const sprite = new THREE.Sprite(material);
        sprite.scale.set(14, 14);
        return sprite;

      } else if ((node.type ?? null) === null) {
        const sprite = new SpriteText(node.id);
        sprite.material.depthWrite = false; // make sprite background transparent
        sprite.color = node.color;
        sprite.textHeight = 4;
        sprite.center.y = -1.2; // shift above node
        return sprite;
      }
    });


  Graph.d3Force('charge').strength(-120);
}

const data = {
  "nodes": [{
      "label": "Interdisciplinary research",
      "x": 1072,
      "y": -498,
      "id": "Interdisciplinary research",
      "type": "keywords",
      "color": "rgb(0,189,148)",
      "size": 33
    },
    {
      "label": "Prof. Dr. J. H.",
      "x": -1448,
      "y": -4560,
      "id": "Prof. Dr. J. H.",
      "type": "scholar",
      "color": "rgb(0,189,148)",
      "size": 30
    },
    {
      "label": "Computer science",
      "x": -2228,
      "y": 13,
      "id": "Computer science",
      "type": "discipline"
      "color": "rgb(120,29,248)",
      "size": 30.0
    },
    {
      "label": "#coding-trouble",
      "x": -15396,
      "y": 16305,
      "id": "#coding-trouble",
      "type": "keyword",
      "color": "rgb(192,192,192)",
      "size": 31
    }
  ],
  "links": [{
      "source": "Prof. Dr. J. H.",
      "target": "Interdisciplinary research",
      "id": "988",
      "color": "rgb(0,189,148)",
      "size": 1.0
    },
    {
      "source": "Prof. Dr. J. H.",
      "target": "#journalism",
      "id": "555",
      "color": "rgb(192,192,192)",
      "size": 1.0
    },
    {
      "source": "Benjamin St.",
      "target": "#journalism",
      "id": "1555",
      "color": "rgb(92,192,192)",
      "size": 1.0
    }
  ]
};


main(data);
body {
  margin: 0;
}
<div id="3d-graph"></div>

<img src='https://ibb.co/JWP3VSv' id='hashtag'></p>
<img src='https://ibb.co/5g6FdHQ7' id='discipline'></p>

一位友好用户 @kikon制作了这个fiddle的最小可运行示例来表示我的问题:https://jsfiddle.net/otpx0jg9/ 以及一个可能的解决方案https://jsfiddle.net/2z80njf1。

解决方案

问题被证明是双重的:核心问题是如何把两个用户生成的sprite结合起来,以通过其 nodeThreeObject 方法来渲染 ForceGraph3D 的节点。原帖的代码已经提供了sprite:一个是图片,另一个是文本sprite。

这里的一个解决方案是使用sprite的 add 方法,这是Three.js的标准 Object3D.add

new ForceGraph3D(elem)
   .........
   .nodeThreeObject(node =>{
       ......
       const textSprite = new SpriteText(node.label ?? node.id);
       const imgTexture = new THREE.TextureLoader().load(.......);
       imgTexture.colorSpace = THREE.SRGBColorSpace;
       const material = new THREE.SpriteMaterial({map: imgTexture});
       sprite = new THREE.Sprite(material);
       ......
       const textSprite = new SpriteText(node.label ?? node.id);
       ......
       sprite.add(textSprite);
   })
   .........

这意味着文本sprite的尺寸和定位需要调整,以便与被添加的图片sprite的缩放和定位相匹配。


次要问题(在结果可视化中出现了一些奇怪的伪影后产生)是 如何移除节点默认的球体表示

为此,应使用 .nodeThreeObjectExtend 方法(在文档中与同一“Node Styling”标题下的 nodeThreeObjet 一起出现)。它允许有选择地仅对那些有合理理由的节点隐藏原始球体——在本例中,原因是它干扰了用于表示这些节点的自定义图片。

new ForceGraph3D(elem)
   .........
   .nodeThreeObjectExtend(node =>{
      return !(["discipline", "keywords", "structure"].includes(node.type));
   })
   .........

在这里,只有类型为 "discipline""keywords""structure" 的节点才会禁用球体。

以下是一个工作解决方案(在codesandbox中,因为它依赖多种资源、图片、json文件),它修改了原始fiddle——基于原始帖子提出的MRE,在我们试图澄清问题时——使用一个复杂对象(specialTypes)来表示需要特殊渲染的每种节点类型的属性,以避免重复代码(在原始代码中,这些类型的代码几乎相同,唯一的差别是数据,例如用于sprite的文件名、缩放、文本高度等)。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章