[SOLVED] How to check if a 3D-object is visible?

Since chatGPT doesn’t find a working solution, maybe some humans can answer my question :slight_smile:

How do I check (by Script) if a 3D-object is visible or not?

The goal is to label 3D-objects by a HTML-DOM-Element which follows the 3D-object. But if it disapears behind other objects then the HTML-Element should disapear as well.

The reason why I wanna use HTML-Elements to label objects, is because they are cripser and clearer and easier to style than real 3D-labels.

Does anybody know how to handle this?

Kind regards
Alain

Hi @Alain!

Maybe the topic below can help you, but I’m not sure if it only checks whether the entity is inside the camera’s viewport or if it also determines whether it’s occluded by other objects.

Otherwise, if it’s just a few objects, you could try doing a raycast from the camera to the object to see if the ray actually hits it.

1 Like

Yes, what @Albertos shared will check if it’s inside the camera frustum or not.

There isn’t a built in way to do occlusion culling, that is check if it’s behind other models, in PlayCanvas at the moment.

1 Like

Thank you guys for the hints.
Found a solution now.

ChatGPT sometimes makes simple things complicated :sweat_smile:

Can you please share it, to help others with the same problem?

How can I share a project? Can’t find it :thinking:

You can just share some information about your solution to help point others in the right direction. If you wish, you can also include a bit of code or a browser link if you have a public sample project.

Ok, here is the working example: Test-Hiding-DOM-Elements - PLAYCANVAS

With the help of you and chatGPT I created this script (the other scripts are out of the box and just for navigation purposes).

Code of domlabel.js:

var DomLabelRay = pc.createScript('domLabelRay');

DomLabelRay.attributes.add('targetEntity', { type: 'entity', title: 'Referenz-Entity (kleiner Cube)' });
DomLabelRay.attributes.add('cameraEntity', { type: 'entity', title: 'Kamera' });
DomLabelRay.attributes.add('text', { type: 'string', default: 'Hallo!', title: 'Label-Text' });

DomLabelRay.prototype.initialize = function () {
    if (!this.targetEntity || !this.cameraEntity || !this.cameraEntity.camera) {
        console.error('[DomLabelRay] targetEntity oder cameraEntity fehlt.');
        return;
    }
    if (!this.app.systems.rigidbody || !this.app.systems.rigidbody.raycastFirst) {
        console.error('[DomLabelRay] Physics nicht aktiv – aktiviere Project Settings > Physics.');
        return;
    }

    // DOM-Element (Inline-Style, fixe Größe)
    this.el = document.createElement('div');
    this.el.textContent = this.text;
    document.body.appendChild(this.el);

    const s = this.el.style;
    s.position = 'fixed';
    s.width = '140px';
    s.height = '36px';
    s.background = 'rgba(255,0,0,0.9)';
    s.color = '#fff';
    s.font = '14px/36px system-ui, sans-serif';
    s.textAlign = 'center';
    s.borderRadius = '6px';
    s.pointerEvents = 'none';
    s.zIndex = '9999';
    s.transform = 'translate(-50%, -100%)'; // über dem Punkt
    s.display = 'none';

    this.canvas = this.app.graphicsDevice.canvas;
    this._sp = new pc.Vec3();

    this.on('destroy', function () {
        if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
    }, this);
};

DomLabelRay.prototype._belongsToTarget = function (ent) {
    for (var e = ent; e; e = e.parent) if (e === this.targetEntity) return true;
    return false;
};

DomLabelRay.prototype.update = function (dt) {
    const camEnt = this.cameraEntity;
    const cam = camEnt.camera;

    // 1) 3D -> Screen (Canvas-Pixel, Ursprung unten links)
    cam.worldToScreen(this.targetEntity.getPosition(), this._sp);

    // hinter Kamera / außerhalb Canvas => aus
    if (this._sp.z < 0 ||
        this._sp.x < 0 || this._sp.x > this.canvas.width ||
        this._sp.y < 0 || this._sp.y > this.canvas.height) {
        this.el.style.display = 'none';
        return;
    }

    // 2) Ray durch GENAU diesen Pixel (wie dein onMouseMove)
    var from = cam.screenToWorld(this._sp.x, this._sp.y, cam.nearClip);
    var to   = cam.screenToWorld(this._sp.x, this._sp.y, cam.farClip);

    var hit = this.app.systems.rigidbody.raycastFirst(from, to);
    var visible = !!hit && this._belongsToTarget(hit.entity);

    // 3) DOM-Position mit LEFT + BOTTOM (gleichläufiges Y)
    if (visible) {

        const rect = this.canvas.getBoundingClientRect();
        const dpr = window.devicePixelRatio || 1;

        // Korrektur: worldToScreen arbeitet im Backbuffer (device pixels)
        // rect.{width,height} sind CSS-Pixel -> umrechnen via DPR
        const scaleX = rect.width  / (this.canvas.width  / dpr);
        const scaleY = rect.height / (this.canvas.height / dpr);

        const cssLeft   = rect.left + (this._sp.x * scaleX);
        const gapBottom = window.innerHeight - (rect.top + rect.height);
        const cssBottom = gapBottom + (this._sp.y * scaleY);

        const s = this.el.style;
        s.left   = cssLeft + 'px';
        s.top = cssBottom + 'px';   // ✅ bottom statt top
        s.display = 'block';
    } else {
        this.el.style.display = 'none';
    }
};


This are the 3D-Bodies I wanted to label. They have a Rigidbody- and a Collision-Component and the script attached.

The 3D-objects which should hide the labled objects, need a Rigidbody- and a Collision-Component as well.

In Rendersettings you have enable physics (aka ammo.js).

That’s all.

Kind regards, Alain

3 Likes

That’s great! Thanks a lot! :pray: