Inconsistent face orientation on transparent double sided shader material on a glb model

Hey, I spent like three days with this issue, I think there is a bug on ShaderMaterial (v0.172.0) but hopefully there is some tweak to fix it.

The issue: inconsistent face orientation on transparent double sided shader material on a glb model. It works fine for front/back side, also using MeshStandardMaterial.

What I expect: this is how it looks with FrontSide, no inconsistency on face orientation.

Vertex normals and face normals are ok (tested on Rhino3d, Blender and online glb viewers). Also checked vertex winding order, whick are ok.

The code: this is a minimal code to see the issue. I can’t get rid of double sided (well, I could duplicate the mesh an make a material for the front and other for the back, but performance is a big issue for my purpouse). The shader code is also a minimal case to represent the problem, tried playing with gl_FrontFacing and I can’t discard a side since expected material is a diamond with refraction and reflection. I tried different models with the same problem, I also tried to change everything on the mesh creation on Rhino/Grasshopper before exporting to glb. I also tried every suggestion from ChatGPT without any success.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color('white');

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 22, 10); 

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;

const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);

const vertexShader = `
varying vec3 vNormal;
void main() {
    vNormal = normalMatrix * normal;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = `
varying vec3 vNormal;
void main() {
    gl_FragColor = vec4(vec3(0.0), 0.5);
}
`;

const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
transparent: true,
side: THREE.DoubleSide,
});

const loader = new GLTFLoader();
loader.load(
    '/model.glb',
    (gltf) => {
        const model = gltf.scene;
        model.position.set(0, -5, 0);
        scene.add(model); 
        model.traverse((child) => {
            if (child.isMesh) {
                child.material = material;
                //child.material = new THREE.MeshStandardMaterial({transparent: true, opacity: 0.5, side: 2}); // this works fine so I guess it's not a model issue.
            }
        });
    },
    undefined,
    (error) => {
        console.error('An error occurred while loading the GLB model:', error);
    }
);
 
function animate() {
    requestAnimationFrame(animate); 
    renderer.render(scene, camera);
}
animate();

The model:
model.glb (667.9 KB)

The project:
shadermatbug.zip (206.8 KB)

Please help!

look like a classic depth sorting issue to me.
Such level of transparency is a complex problem, three.js can’t render this properly with the default renderer. Some solution exists (someone gathered them in a single post, but i can’t retrieve it) none is perfect tho.

It’s recommended to avoid more than 2-3 transparent superposed objects on screen (alphaTest is fine). And they should be simple like cubes or quads. Diamonds will get you into troubles.

1 Like

Thanks for the reply.
Not sure, I also tried to play with renderOrder and it’s the same problem for a glb with a single mesh:

Also the fact that MeshStandardMaterial works as expected makes me think it’s a ShaderMaterial issue.

DoubleSided + transparent will show this issue due to the triangles being specified in an arbitrary order. It’s not a bug.

You can work around it by using 2 copies of the mesh each with their own material… one material set to material.side = THREE.FrontSide
and the other mesh.material.side = THREE.BackSide

Why it works as expected on other types of materials then? But I can confirm that by changing the order of triangles it change the ones who are back faced.

Thanks.

Oh i see… you have a bunch of intersecting planes.
That’s a different issue.

Try disabling depthTest.

material.depthTest = false;

Not sure what you mean by that. Each face has three distinct vertex (meaning that there are multiple points on the same location, one per adjecent face so that vertex normals are perpendicular to diamond facets). There is no duplicated faces.

Yeah that avoid the inconsistency but it puts the trasnparent objects on top so it doesn’t work. I’m making a ring scene, not just a diamond.

Thanks.

Try:

material.depthWrite = false;

instead of material.depthTest = false;

also try:
material.blending = THREE.AdditiveBlending;
material.opacity = .5;

:smiley:

depthWrite=true
blending=Normal

depthWrite=false
blending=Normal
I guess this can be improve by shader code but the insconsistency is still there.

depthWrite=false
blending=Addivtive

The first image is what I’m tring to fix.

and for the first one you’re doing:

side:THREE.DoubleSide
depthWrite:false

?

doubleSide and depthWrite: true (you can check the GUI to confirm).

Does setting material.forceSinglePass to true vs. false on the ShaderMaterial make a difference?

3 Likes

Wow material.forceSinglePass = false; solved the issue! I owe you a :beer:.

Thank you so much all!

1 Like

Does this glitch show the same issue? I tried sorting the geometry in blender…

https://bottlenose-universal-mask.glitch.me

It looks fine! what did you change in the geometry? Let’s see if I can replicate it on Grasshopper3d.

Ouch, the transparency stop working when forceSinglePass is false.
forceSinglePass = false

forceSinglePass = true

Changing transparency from GUI doesn’t do anything, despite the shader code force it:

gl_FragColor = vec4(finalColor,  transparency);

I downloaded your glb and it has the same issue on my script:


I’ll try to find any relevant differences.