Vasturiano 3d-force-graph多类型节点显示整合问题:自定义PNG、默认精灵与文本标签适配及着色解决方案问询
解决3d-force-graph自定义Sprite/图片节点的显示与着色问题
我仔细看了你的代码和需求,发现几个关键的bug和逻辑问题,导致自定义图片被覆盖、标签不显示,以及无法着色的问题。下面是完整的修复方案,完美匹配你的4个需求:
先说说代码里的核心问题
- 重复声明
const img变量,导致discipline图片覆盖了hashtag的引用 - 类型判断不匹配:你的节点数据里是
type: "keyword"/"discipline"/"scholar",但代码里写的是node.type == "keywords"(多了个s) - Scholar分支错误加载了discipline的图片,逻辑完全搞反了
- 自定义节点时没有手动添加文本标签,且
nodeThreeObjectExtend的用法需要调整 - 没有开启SpriteMaterial的着色功能,导致无法用
node.color给PNG上色
修复后的完整代码
HTML
<div id="3d-graph"></div> <!-- 不需要提前加载img标签,我们直接在Three.js中加载纹理 -->
CSS
body { margin: 0; }
JavaScript
import SpriteText from "https://esm.sh/three-spritetext"; import * as THREE from "https://esm.sh/three"; import ForceGraph3D from "https://cdn.jsdelivr.net/npm/3d-force-graph"; function main(data) { const elem = document.getElementById('3d-graph'); // 预加载纹理,避免重复加载 const textureLoader = new THREE.TextureLoader(); const hashtagTexture = textureLoader.load('https://ibb.co/JWP3VSv'); hashtagTexture.colorSpace = THREE.SRGBColorSpace; const disciplineTexture = textureLoader.load('https://ibb.co/5g6FdHQ7'); disciplineTexture.colorSpace = THREE.SRGBColorSpace; const Graph = new ForceGraph3D(elem) .graphData(data) // 关闭默认标签,我们手动添加自定义标签 .nodeLabel(null) .nodeThreeObjectExtend(false) // 不需要扩展默认节点,完全自定义 .onNodeClick(node => { 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 }; Graph.cameraPosition(newPos, node, 3000); }) .nodeAutoColorBy('group') .nodeThreeObject(node => { const group = new THREE.Group(); // 1. 创建节点主体 let nodeSprite; if (node.type === "keyword") { // 关键词节点:加载hashtag图片 const material = new THREE.SpriteMaterial({ map: hashtagTexture, color: new THREE.Color(node.color), // 用node.color着色 transparent: true, // 保留PNG透明度 blending: THREE.AdditiveBlending // 优化透明效果 }); nodeSprite = new THREE.Sprite(material); nodeSprite.scale.set(14, 14); } else if (node.type === "discipline") { // 学科节点:加载discipline图片 const material = new THREE.SpriteMaterial({ map: disciplineTexture, color: new THREE.Color(node.color), transparent: true, blending: THREE.AdditiveBlending }); nodeSprite = new THREE.Sprite(material); nodeSprite.scale.set(14, 14); } else if (node.type === "scholar") { // 学者节点:默认彩色球形精灵 const geometry = new THREE.SphereGeometry(7, 16, 16); const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(node.color) }); nodeSprite = new THREE.Mesh(geometry, material); } group.add(nodeSprite); // 2. 添加文本标签(所有节点都添加) const textSprite = new SpriteText(node.label); textSprite.material.depthWrite = false; textSprite.color = node.color; textSprite.textHeight = 4; textSprite.center.y = -1.2; // 标签在节点上方 textSprite.position.y = 10; // 调整标签与节点的间距 group.add(textSprite); return group; }) // 节点大小控制 .nodeSize(node => node.size / 2); // 根据你的size值调整缩放比例 Graph.d3Force('charge').strength(-120); } const data = { "nodes": [{ "label": "Interdisciplinary research", "x": 1072, "y": -498, "id": "Interdisciplinary research", "type": "keyword", // 修正为keyword,和代码判断匹配 "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": "#coding-trouble", "id": "555", "color": "rgb(192,192,192)", "size": 1.0 }, { "source": "Prof. Dr. J. H.", "target": "Computer science", "id": "1555", "color": "rgb(120,29,248)", "size": 1.0 }] }; main(data);
关键改动说明
- 预加载纹理:用同一个TextureLoader预加载两张图片,避免重复加载,提升性能
- 修正类型判断:将
keywords改为keyword,匹配数据里的type值,同时修正scholar和discipline的逻辑错误 - 自定义节点组:用
THREE.Group将节点主体和文本标签组合在一起,确保两者一起显示 - 图片着色实现:在SpriteMaterial中设置
color: new THREE.Color(node.color),同时开启transparent: true保留PNG的透明区域 - 学者节点处理:用Three.js的SphereMesh实现默认彩色球形,匹配你的需求
- 文本标签统一添加:所有节点都手动添加SpriteText标签,设置颜色和位置,解决标签缺失问题
- 关闭默认标签:设置
.nodeLabel(null)避免默认标签和自定义标签冲突 - 节点大小调整:通过
.nodeSize()根据数据里的size值缩放节点,保证显示比例合理
这样调整后,你的三个节点类型都会按照需求正确显示,PNG图片也能被node.color着色,文本标签正常显示在节点上方。
内容的提问来源于stack exchange,提问作者greg.gan




