在3d-force-graph中使用.nodeThreeObject将多个节点的显示合并在一起
我正在研究Vasturiano的(令人震撼、如同被魔法迷住的)3d-force-graph,在把三个看起来非常简单的元素:一个sprite、一个图片,以及图片上的文本,组合在一起、再分离开来时遇到了困难。
除了可视化中表示的“学者”之外,我还想用图片来表示“关键词”(使用./hashtag.png)和“学科领域”(使用./discipline.png)。最终的情况是,关键词(./hashtag.png)和学科(./discipline.png)会与精灵图片叠加在一起,导致没有显示任何文本标签:#keyword被错误地与默认节点精灵叠加,标签缺失。
- 我希望每个学者使用默认的精灵(彩色球)来显示,在其上方放置
node.color文本。 - 我希望每个关键词使用
/imgs/hashtag.png图像来显示,在其上方放置node.color文本。 - 我希望每个学科领域使用
/imgs/disipline.png显示,在其上方放置node.color文本。
使用PNG也意味着我不能像把它们作为精灵那样给它们着色 node.color,这点我也想修正。我应该创建一组新的精灵(如果要,应该怎么做?),还是修改现有代码来分离并标注这三种不同的组?
这是完整的可视化,但请记住我只是在测试它,.json文件条目已手动编辑以包含 type:https://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的文件名、缩放、文本高度等)。