67
votes

I am trying to write a small program in Three.js that displays two spheres, one inside the other. The radius of sphere2 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 sphere2 varies.

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

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

Is there any 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);
4

4 Answers

167
votes

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

10
votes

A couple comments.

First. If you are going to ask a question that expects people to review code, put it in 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 in 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);

What I've changed in your code is to 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 2 links

WebGL transparent planes Transparent texture behavior

0
votes

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.

-1
votes

All you have to do is:

mesh.transparent = true; //This must be set to true
mesh.opacity = 1; //Value from 0-1

Then you can control these attributes with dat.GUI and whatever

-Anayttal