Alain
October 7, 2025, 2:21pm
1
Since chatGPT doesn’t find a working solution, maybe some humans can answer my question
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.
Thanks @Leonidas and @LeXXik , i will surely try the engine property to check how much it effects my game performance.
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
Alain
October 7, 2025, 4:01pm
4
Thank you guys for the hints.
Found a solution now.
ChatGPT sometimes makes simple things complicated
Alain:
Found a solution now.
Can you please share it, to help others with the same problem?
Alain
October 8, 2025, 10:36am
6
How can I share a project? Can’t find it
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.
Alain
October 13, 2025, 12:06pm
8
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!