80

I am trying to write a small program in Three.js that displays two spheres, one inside the other. The radius of sphere 2 is supposed to oscillate between 0.5 and 1.5 while the radius of sphere1 is always 1.0. Each sphere is transparent (opacity: 0.5) so that it would be possible to see the smaller sphere contained in the larger one. Of course the roles of "smaller" and "larger" change as the radius of sphere 2 varies.

The problem now is that Three.js makes the first sphere transparent. I define in my program but not the second one. If I first define sphere 1 then it becomes transparent, but then sphere 2 is completely opaque. If I first define sphere 2 then this is the transparent one. The order of adding them to the scene plays no role.

I include a minimal program below that shows what is going on (without the animation). In its current state only sphere 1 is visible and it is not transparent. If I define sphere 1 before sphere 2 then sphere 1 becomes transparent, but sphere 2 is no longer transparent. Changing sphere 2's radius to 1.2 will then hide sphere 1.

Is there a way to make both spheres transparent?

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);

camera.position.set(0, 0, 3);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);

var ambient = new THREE.AmbientLight( 0x555555 );
scene.add(ambient);

var light = new THREE.DirectionalLight( 0xffffff );
light.position = camera.position;
scene.add(light);

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

// Definition 2
var geometry2 = new THREE.SphereGeometry(0.8,32,24);
var material2 = new THREE.MeshLambertMaterial({color: 0x0000ff, transparent: true, opacity: 0.5});
var sphere2 = new THREE.Mesh(geometry2, material2);

// Definition 1
var geometry1 = new THREE.SphereGeometry(1.0,32,24);
var material1 = new THREE.MeshLambertMaterial({color: 0x00ff00, transparent: true, opacity: 0.5});
var sphere1 = new THREE.Mesh(geometry1, material1);

scene.add(sphere1);
scene.add(sphere2);

renderer.render(scene, camera);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
cefstat
  • 2,336
  • 1
  • 18
  • 25

4 Answers4

190

Both your spheres are transparent, and are remaining so. What is happening is that the smaller sphere is not being rendered at all.

Transparency in WebGL is tricky. You can google the issue to find out more about it.

But you have stumbled upon an issue related to how three.js in particular handles transparency.

The WebGLRenderer in three.js sorts objects based upon their distance from the camera, and renders transparent objects in order from farthest to closest. (This is an important point: It sorts objects based on their position, and renders objects in the sorted order.)

So for two transparent objects to render correctly, the object that is in back -- the smaller sphere in your case -- must be rendered first. Otherwise, it will not be rendered at all, due to the depth buffer.

But in your case, you have two spheres that are in the same location, and hence are equidistant from the camera. That is the problem -- which one to render first; it is a toss-up.

So you need to place the smaller sphere further away from the camera than the larger sphere in order for the scene to render correctly.

One solution is to move the smaller sphere back a little.

Another solution is to set renderer.sortObjects = false. Then the objects will render in the order they are added to the scene. In that case, be sure to add the smaller sphere to the scene first.

A third solution is to set material1.depthWrite = false and material2.depthWrite = false.

EDIT:

Renderable objects having material.transparent = false (opaque objects) are rendered before objects having material.transparent = true (transparent objects).

So a fourth solution is to make the smaller sphere opaque so it is rendered first.

New feature for r.71:

There is now an Object3D.renderOrder property. Within each class of object (opaque or transparent), objects are rendered in the order specified by object.renderOrder. The default value of renderOrder is 0. Note that renderOrder is not inherited by child objects; you must set it for each renderable object.

Objects with the same renderOrder (ties), are sorted by depth, as described above.

So a fifth solution is to set renderOrder = 1 for the larger sphere. This is likely the best solution in your case.

three.js r.71

WestLangley
  • 102,557
  • 10
  • 276
  • 276
  • What does depthWrite do exactly ? What are the drawbacks (perfs, new bugs, ...?) ? – Maël Nison Aug 23 '13 at 21:26
  • I think depth write signals that the objects should write their own depths, meaning that they figure out their depth in a scene based on their distance to the camera. If you disable it, you can override a new depth – Jack Franzen Oct 12 '13 at 23:01
  • 6
    The `renderOrder` feature in r.71 worked for me. NOTE: You have to set it on children objects. Setting it on a parent object does not apply to the children. – Brian Mar 26 '15 at 21:16
  • 8
    Adding `depthWrite: false` was the solution, thanks! So I am using now: `var material_plane = new THREE.MeshBasicMaterial({color: drawcol, side: THREE.DoubleSide, opacity: 0.5, transparent: true, depthWrite: false});` – Avatar Sep 02 '15 at 21:34
  • @Matheretter It is preferred that you up-vote answers on this site (if you want), instead of adding 'thank you' comments. – WestLangley Sep 02 '15 at 22:36
  • I actually added some more code to help others figuring out how to use one of the solutions correcly. That was not a pure thank you... upvote done. – Avatar Sep 03 '15 at 20:41
  • @WestLangley thanks for answer this transparency issue was making me nuts and the situation worse when have multiple transparent object render one by one many thanks for renderOrder – Ajit kohir Mar 31 '16 at 17:33
  • 1
    depthwrite fixed it for me – Jeff - Software Results Jul 27 '18 at 00:45
  • Oh dear... this solved hours and hours of work... Please accept my sincere gratitude, dear @WestLangley and Everyone who participated... You are astronomically magnificent! Please have a pleasant time and stay safe! Related: https://stackoverflow.com/questions/50690473/threejs-mesh-becomes-invisible-when-inside-another-mesh – Artfaith Feb 14 '23 at 08:57
  • Thank you, I fixed a bug that was there since 2 years using renderOrder = 1! – Loreppo May 15 '23 at 13:56
  • I needed an opaque object to render ontop of a transparent object, so I ended up setting `material.transparent = true` on the opaque object to ensure to `renderOrder` of the opaque object places it ontop of the transparent object. The fact that `transparent = true` objects are rendered after `transparent = false` objects was the missing piece of the puzzle. – Atlinx Jun 22 '23 at 16:31
12

A couple of comments.

First. If you are going to ask a question that expects people to review code, put it on JSFiddle. If you do, you will get more people taking a peek. That having been said, here is a slightly modified version of your code on JSFiddle, please use it as a guide for future questions. JSFiddle example

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);

camera.position.set(0, 0, 3);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);

var ambient = new THREE.AmbientLight(0x555555);
scene.add(ambient);

var light = new THREE.DirectionalLight(0XFFFFFF);
light.position = camera.position;
scene.add(light);

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

renderer.sortObjects = false;

// Definition 2
var geometry2 = new THREE.SphereGeometry(0.8, 32, 24);
var material2 = new THREE.MeshLambertMaterial({color: 0x0000ff, transparent: true, opacity: 0.5});
var sphere2 = new THREE.Mesh(geometry2, material2);

// Definition 1
var geometry1 = new THREE.SphereGeometry(1.0, 32, 24);
var material1 = new THREE.MeshLambertMaterial({color: 0xff0000, transparent: true, opacity: 0.5});
var sphere1 = new THREE.Mesh(geometry1, material1);

scene.add(sphere2);
scene.add(sphere1);

renderer.render(scene, camera);

In your code I've set sortObjects to false and then changed the order that the spheres were added to the scene. This was done because of the information in the next two links:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
John McKnight
  • 684
  • 4
  • 9
  • 1
    Thank you for taking the time to check my code and reply. Setting `rendered.sortObjects` to `false` makes the final result depend on the order I add the objects to the scene, as explained in the later answer by WestLangley. The problem is that I want to animate the scene and for some frames it would be better to have one sphere drawn first and fore some other frames the other sphere. – cefstat Apr 14 '13 at 04:00
2

For me it works with a transparent pix... With others solutions i've got artefact with my "clouds shaders"

1/ Wireframe:

    <meshBasicMaterial wireframe />

enter image description here

2/ Opacity & transparency:

<meshBasicMaterial opacity="0" transparent="true" thickness="0" transmission="0" depthTest={true}/>

enter image description here

3/ Map with transparent texture:

    import { useLoader } from '@react-three/fiber'
    import pixTrans from './assets/pix.png' // transparent pixel
    const [textureTrans] = useLoader(THREE.TextureLoader, [pixTrans]) 
    
    return (
        <mesh position={[0, 2, 0]}>
          <boxGeometry args={[3, 5, 3]}/>
          <meshBasicMaterial map={textureTrans} alphaTest="0.5" />
        </mesh>
    )

enter image description here

molokoloco
  • 4,504
  • 2
  • 33
  • 27
1

For what it's worth I could not solve the same problem using the methods above but found that having:

scene = new THREE.Scene();
group = new THREE.Group();
scene.add(group);

in my init() and then adding the frontside mesh to the scene, but the backside mesh to group solved the problem. i.e.:

var materialfront = new THREE.MeshPhongMaterial({
                           opacity:1, map:texture});
materialfront.transparent = true;
materialfront.side = THREE.FrontSide;
frontthing = new THREE.Mesh(geometry, materialfront);
frontthing.renderOrder = 2;
scene.add(frontthing);

then

var texture2 = texture.clone();
texture2.needsUpdate = true;
var materialBack = new THREE.MeshPhongMaterial({
                                opacity:0.1, map: texture2})
materialBack.transparent = true;
materialBack.side = THREE.BackSide;
backthing = new THREE.Mesh(geometryback, materialBack);
backthing.renderOrder = 1;
group.add(backthing);

My material map was a transparent .png texture.

I can not explain why other suggested methods did not work for me, but I hope the above might help some one in a similar position.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131