Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS2D/3DRenderer: Ensure Object3D.visible is correctly evaluated in object hierarchies. #28293

Merged
merged 9 commits into from
May 9, 2024

Conversation

lafflan
Copy link
Contributor

@lafflan lafflan commented May 6, 2024

…stors are visible

Related issue: #XXXX

Description

A clear and concise description of what the problem was and how this pull request solves it.

This contribution is funded by Example

@lafflan lafflan changed the title CSS2DObject and CSS3DObject visibility needs to consider whether ance… May 6, 2024
@Mugen87
Copy link
Collaborator

Mugen87 commented May 6, 2024

For reproduction: https://jsfiddle.net/nhg8durq/

@Mugen87
Copy link
Collaborator

Mugen87 commented May 6, 2024

Um, I'm not sure why the CSS renderers behave differently compared to WebGLRenderer. Normally, when Object3D.visible is set to false neither the object nor its descendant nodes should be rendered.

It's also better to fix this issue in renderObject(). Maybe just adding the following line at the top of the method in CSS3DRenderer:

if ( object.visible === false ) return;

For CSS2DRenderer it should be:

if ( object.visible === false || ( _vector.z < - 1 || _vector.z > 1 )) return;

The local visible property in renderObject() only depends from the layers tests like in WebGLRenderer.

@yomotsu Do you know why the CSS renderers handle children of invisible objects differently than WebGLRenderer?

@yomotsu
Copy link
Contributor

yomotsu commented May 6, 2024

Actually, I'm not entirely sure, and it seems that the potential problem might have gone unnoticed.
The issue is attributed to the following reasons:

  • Setting visible = false will set display: none on the CSS3DObject internally, and its DOM children will not be rendered either.
    see:
    object.element.style.display = ( visible === true ) ? '' : 'none';
  • However, nested CSS3DObjects are not actually nested in the DOM scene graph but are flattened using a global matrix.
  • Therefore, setting visible = false on a parent CSS3DObject does not affect its children.

I thought nested CSS3DObjects are also nested in the DOM scene graph and parents display: none should affect its children, but not.

Given these points, I think each CSS3DObject needs to check its ancestors' visible as this PR does.
Alternatively, CSS3DObjects could have a .visible setter that changes the CSS display property ('none' or 'block') of the element and its descendants (no need to check ancestors at every render).

@Mugen87
Copy link
Collaborator

Mugen87 commented May 7, 2024

Given these points, I think each CSS3DObject needs to check its ancestors' visible as this PR does.

The solution would be more consistent when the same coding patterns are applied like in WebGLRenderer. So if the code from #28293 (comment) would also work, I would prefer that approach.

@yomotsu
Copy link
Contributor

yomotsu commented May 7, 2024

That makes sense and I tested adding that.

function renderObject( object, scene, camera, cameraCSSMatrix ) {

	if ( object.visible === false ) return;	

The above will stop the rendering process of children, However, CSS3DObjects will remain rendered without matrix updates because DOM elements are rendered because HTML is rendered in "retained mode", unlike WebGL or Canvas2D.
If we take the above approach, I think all CSS3DObjects must be hidden before rendering, then, show them if visible === true

e.g.

this.render = function ( scene, camera ) {

  // snip
  hideAllObjects( scene ); // traverse and hide all objects regardless its visible state
  renderObject( scene, camera, cameraCSSMatrix ); // will render if object is visible. the process will be stopped if visible === false

};

@Mugen87 Mugen87 added this to the r165 milestone May 7, 2024
@lafflan
Copy link
Contributor Author

lafflan commented May 8, 2024

Do we still need to change the Object3D core code like this:

class Object3D{
  constructor(){
    ...
    // this.visible=true;
    this._visible=true;
    ...
  }
  set visible(val) {
    this._visible = val;
    // no need to traverse children's children,  becasuse they also have visible setter
    const children = this.children;
    for (let i = 0, l = children.length; i < l; i++) {
        children[i].visible = val; 
     }
  }

  get visible() {
    return this._visible;
  }
  ...
}
@Mugen87
Copy link
Collaborator

Mugen87 commented May 8, 2024

Let's not apply any changes to Object3D and focus on the CSS renderers instead.

@Mugen87
Copy link
Collaborator

Mugen87 commented May 8, 2024

The problem with ancestorIsVisible() is it performs a lot of redundant hierarchy traversal.

Do you mind implementing the PR differently based on #28293 (comment)?

@lafflan
Copy link
Contributor Author

lafflan commented May 9, 2024

function renderObject( object, scene, camera ) {

if ( object.visible === false ) return;
Copy link
Collaborator

@Mugen87 Mugen87 May 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the ( object.visible === true ) && check further below can be removed now, right?

Copy link
Contributor

@yomotsu yomotsu May 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. The object has to be visible === true below this line, so there's no need to check again.
Thus, line 138 can be

- const visible = ( object.visible === true ) && ( _vector.z >= - 1 && _vector.z <= 1 ) && ( object.layers.test( camera.layers ) === true );
+ const visible = ( _vector.z >= - 1 && _vector.z <= 1 ) && ( object.layers.test( camera.layers ) === true );
Copy link
Contributor

@yomotsu yomotsu May 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, the line that sets the CSS display value can look like this,
because all elements are already set to display: none due to hideObject(object)

- object.element.style.display = ( visible === true ) ? '' : 'none';

if ( visible === true ) {

 	object.onBeforeRender( _this, scene, camera );

	const element = object.element;

+	element.style.display = '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful!

@Mugen87 Mugen87 merged commit 66a31ae into mrdoob:dev May 9, 2024
11 checks passed
@Mugen87 Mugen87 changed the title CSS2DObject and CSS3DObject visibility needs to consider whether ancestors are visible May 9, 2024
@lafflan
Copy link
Contributor Author

lafflan commented May 10, 2024

@Mugen87 There is another problem in CSS2DRenderer.js 's zOrder method:

function zOrder(scene) {
  ...
  const distanceA = cache.objects.get(a).distanceToCameraSquared;
  const distanceB = cache.objects.get(b).distanceToCameraSquared;
  ...
}

Due to the cache setting condition that objects must be visible (including their ancestors), when an object is initially invisible ,the cache.objects.get(object) will return undefined, and the above code will report an error:
1715329716694

So it is necessary to check if the cache is empty and set a default value , some code like this:

const cacheDataA = cache.objects.get(a);
const distanceA = cacheDataA ? cacheDataA.distanceToCameraSquared : 0;
const cacheDataB = cache.objects.get(b);
const distanceB = cacheDataB ? cacheDataB.distanceToCameraSquared : 0;
@Mugen87
Copy link
Collaborator

Mugen87 commented May 10, 2024

Good catch! Would you file another PR with your fix?

@lafflan
Copy link
Contributor Author

lafflan commented May 10, 2024

Done. New PR is #28330 @Mugen87

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
3 participants