Grouped meshes in Three.js are not rendered correctly in shader materials

I am using Three.js to load a GLTF model and implement an overlay effect with ShaderMaterial. However, when I group the meshes using THREE.Group, the meshes disappear when applying the overlay.

Issue:

  • When the meshes are not grouped, they are displayed correctly.
  • When the meshes are grouped, they are not displayed correctly (they disappear).

I would appreciate advice on the following points:

  1. How to properly handle the impact of modelMatrix and normalMatrix when grouping meshes.
  2. How to manage local and world coordinates within a group.
  3. How to address UV mapping or texture issues that might arise when meshes are grouped.

Current observations:

  • Using MeshBasicMaterial instead of ShaderMaterial displays the meshes correctly, but the texture is missing.
  • If the fragment shader uses gl_FragColor = vec4(vWorldPosition.xyz * 0.1, 1.0);, the meshes appear in the correct position.
  • The texture is already included in the GLTF material, so the fragment shader doesn’t need to sample it in order for the model to display.
initializeObj(object: THREE.Object3D) {
    // Process textures
    if (object instanceof THREE.Mesh) {
        this.meshs.push(object);

        // Create a mesh for overlay display
        const overlayMesh = object.clone();

        // Configure the shader material for the overlay
        const matOvelay = new THREE.ShaderMaterial({
            vertexShader,
            fragmentShader,
            depthTest: false, // Disable Z-test to render on top of the original mesh
            transparent: true,
        });

        // Uncomment this block to test with a basic material
        /*
        const matOvelay = new THREE.MeshBasicMaterial({
            color: 0xff0000, // Red color for testing
            depthTest: false,
            transparent: true,
            opacity: 0.5,   // Semi-transparent for testing
        });
        */

        overlayMesh.material = matOvelay;
        // Set render order to ensure the overlay is drawn after the original
        overlayMesh.renderOrder = object.renderOrder + 1;

        // Add the overlay mesh to the parent
        object.parent?.add(overlayMesh);

        this.materialsOverlay.push(matOvelay);
    } else if (object instanceof THREE.Camera) {
        // Set the camera position for the camera dot mesh
        this.cameraDotMesh.position.copy(object.position);
    }

    // Recursively process child objects if they exist
    if (object.children && object.children.length > 0) {
        // Clone the children to avoid modifying the original array
        const children = [...object.children];
        for (const child of children) {
            this.initializeObj(child);
        }
    }
}
// Fragment Shader
// ====================

uniform vec3 p0;
uniform vec3 p1;
uniform vec3 p2;
uniform vec3 p3;
uniform vec3 p4;
uniform vec3 p5;
uniform vec3 p6;
uniform vec3 p7;
uniform vec3 eVec;
uniform int mode;

varying vec4 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;

// Function to check if a point is inside a triangle
bool isPointInside(vec3 p, vec3 pA, vec3 pB, vec3 pC) {
    return dot(cross(pB - pA, pC - pA), p - pA) < 0.0;
}

void main() {
    // Debug: Visualize world position as color
    gl_FragColor = vec4(vWorldPosition.xyz * 0.1, 1.0);
    return;

    // Calculate the dot product between the mesh normal and the container normal (eVec)
    // Map the result from [-1, 1] to [0, 1]
    float dotProduct = dot(normalize(vNormal), normalize(eVec));
    float brightness = dotProduct * 0.5 + 0.5;

    // Allowable angle: 45 degrees
    bool isPlane = brightness > (180.0 - 45.0) / 180.0;

    vec3 point = vWorldPosition.xyz;

    // Check if the point is inside the defined container (formed by triangles)
    bool isInside = isPointInside(point, p0, p1, p2) &&
                    isPointInside(point, p2, p1, p5) &&
                    isPointInside(point, p2, p6, p7) &&
                    isPointInside(point, p5, p1, p0) &&
                    isPointInside(point, p7, p4, p0) &&
                    isPointInside(point, p6, p5, p4);

    // Overlay effect: If the conditions are met, render red, otherwise discard
    if (isPlane && isInside) {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    } else {
        discard;
    }
}
// Vertex Shader
// =================
varying vec4 vWorldPosition;
varying vec3 vNormal;
varying vec2 vUv;

void main() {
    // Transform vertex position to world coordinates
    vec4 worldPos = modelMatrix * vec4(position, 1.0);
    vWorldPosition = worldPos;

    // Calculate the normal vector (transformed using normalMatrix)
    vNormal = normalize(normalMatrix * normal);

    // Pass through UV coordinates
    vUv = uv;

    // Transform vertex to clip space
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

The grouping sounds unrelated. There isn’t much difference between a model being a child of a Group vs being a child of Scene.

Can you post a reproduction of the issue in codepen or jsfiddle?

Thank you for your reply.
I feel that the issue is not unrelated to the problem because things started going wrong after adding meshs to THREE.Group using add() .
Reposting on JSFiddle might take some time, but I will give it a try.

What may change when adding to a Group, is the drawing order of the meshes.. so it may be an order dependent effect.
I’m super curious tho, so if you can throw it in a glitch I wanna see :slight_smile:

Currently, I commented out the following line in the initializeObj() method:

overlayMesh.material = matOvelay;
After doing this, the mesh started to display correctly. Although I still don’t fully understand why this resolved the issue, it is now clear that the problem is not related to the Group.

Additionally, if I remove the object.parent?.add() line, the mesh disappears again. I believe there is something happening in this part of the code.

Thank you for providing hints about the rendering order—it was very helpful.

1 Like