vasturiano / 3d-force-graph-vr

3D force-directed graph component in VR
https://vasturiano.github.io/3d-force-graph-vr/example/large-graph/
MIT License
223 stars 51 forks source link

How to make the nodes look at the camera at all times without calling refresh ? #39

Open slechanii opened 2 years ago

slechanii commented 2 years ago

Hi, i'm trying to make all my nodes (3D meshes) look at the camera position at all times, I am getting the camera position and making the mesh look at its position whenever its rendered the problem is that it only gets executed when nodeThreeObject is called, is there a way for me to make the nodes look at the camera at all times despite not getting rerendered ?

EDIT : After running some tests I found out that while I'm getting a camera, the vector is always (0,0,0) (even atfer moving the camera and triggering a rerender), here is the code I'm using to get the camera and print the position vector inside the nodeThreeObject :

  let cameraEl = document.querySelector("a-entity[camera]");
    if (!cameraEl) {
      cameraEl = document.querySelector("a-camera");
    }
    let camera = cameraEl.components.camera.camera;
    console.log(camera.position) // Always 0,0,0

Thanks.

vasturiano commented 2 years ago

@slechanii have you considered using Sprites? The very definition of a Sprite is a (planar) object that always faces the camera POV, which sounds like what you're seeking.

slechanii commented 2 years ago

@slechanii have you considered using Sprites? The very definition of a Sprite is a (planar) object that always faces the camera POV, which sounds like what you're seeking.

Thanks for answering quickly!

I was indeed using sprites before and it did the job well, the problem is that from I what I saw, using sprites makes it impossible to use hover / onclick events due to a bug with aframe (gathered from past issues and having problems making it work, might have been fixed ?)

If it hasn't been fixed it means that I can't use sprites as I need hover & onclick on nodes for what i'm trying to achieve.

If sprites are not usable do you have an idea on how I could make the nodes always look at the camera ?

Currently I'm facing 2 issues :

  1. I can get a camera (explained in my 1st post) but this camera's position vector is always stuck at (0,0,0) no matter what I do strangely (am I doing it wrong ?)
  2. Even If I were to get the camera and the position vector right, the NodeThreeObject function seems to only be called on rerenders, so I would only be able to make the nodes look at the camera when a rerender is called

Thanks for your help!

vasturiano commented 2 years ago

@slechanii the reason behind the Sprite issue in Aframe is mentioned here: https://github.com/vasturiano/3d-force-graph-vr/issues/31#issuecomment-1043570188

That issue eventually originates in the Aframe module, so there's not a whole lot that can be done to work around it.

Similarly, I'm also not sure how easy it would be to implement Sprite-like behaviour without using actual Sprite objects.

micrology commented 2 years ago

I also encountered this problem and wrote a component to deal with it. Essentially what this does is to wrap each node in an A-Frame <a-sphere>, which makes dealing with them much more tractable. The code below also attaches a <a-text> entity to each sphere, and makes them always look towards the camera.

/* helper for vasturiano/3d-force-graph-vr
    *           draw a sphere around each force graph node, to make it easy to point to them with the ray caster,
    *           and attach a text label (which rotates to always face the camera)
    * after the graph has been created, use something like
    *           fgEl.setAttribute('spherize', {})
    * to create the spheres
    */
AFRAME.registerComponent('spherize', {
    schema: {},
    dependencies: ['forcegraph'],
    init: function () {
        // spheres are cached here and re-used
        this.spheres = new Map()
    },
    tick: function (time, timeDelta) {
        document
            .querySelector('[forcegraph]')
            .components.forcegraph.forceGraph.children.forEach((child) => {
                if (child.type == 'Mesh' && child.__data.id) {
                    let sphereEl = this.spheres.get(child.__data.id)
                    if (sphereEl) {
                        // reuse existing sphere and label, but change its position
                        sphereEl.object3D.position.copy(child.position)
                    } else {
                        sphereEl = document.createElement('a-sphere')
                        sphereEl.classList.add('node')
                        sphereEl.id = child.__data.id
                        this.spheres.set(child.__data.id, sphereEl)
                        sphereEl.setAttribute('position', child.position)
                        let radius = child.geometry.parameters.radius + 0.1
                        sphereEl.setAttribute('radius', radius)
                        let color = child.__data.color || 'white'
                        let compColor = lightOrDark(standardize_color(color)) == 'light' ? 'black' : 'white'
                        sphereEl.setAttribute('color', color)
                        this.el.appendChild(sphereEl)

                        let label = document.createElement('a-entity')
                        label.setAttribute('text', {
                            value: splitText(child.__data.label, 9),
                            color: compColor,
                            width: 5 * radius,
                            align: 'center',
                        })
                        sphereEl.setAttribute('look-at', '#cameraRig')
                        label.setAttribute('position', {x: 0, y: 0, z: radius})
                        sphereEl.appendChild(label)
                    }
                }
            })
    },
})

(the line let compColor = lightOrDark(standardize_color(color)) == 'light' ? 'black' : 'white' sets compColor to white or black depending on which gives a better contrast with the colour of the sphere)