In the referenced fiddle, shadows do not appear unless either (a) the shadow-caster is set to receive shadows, or (b) the render order is reversed.
sphere = new THREE.Mesh( geometry, material );
sphere.castShadow = true;
//sphere.receiveShadow = true; // un-comment to show shadow on plane <=========
scene.add( sphere );
var geometry2 = new THREE.PlaneGeometry( 100, 100 );
var plane = new THREE.Mesh( geometry2, material );
plane.rotation.x = - Math.PI / 2;
plane.position.set( 0, - 5, 0 );
plane.receiveShadow = true;
scene.add( plane );
//plane.renderOrder = - 1; // un-comment to show shadow on plane <=========
fiddle: https://jsfiddle.net/s1nh95y1/
I discovered this when investigating #11387... Not sure if the issues are related.
@WestLangley
var material1 = new THREE.MeshPhongMaterial();
var material2 = new THREE.MeshPhongMaterial();
sphere = new THREE.Mesh( geometry, material1 );
plane = new THREE.Mesh( geometry2, material2 );
Thanks, @RemusMar.
Two materials works, but I'm not sure it is reasonable for users to be expected to know that setting a particular property of the mesh would required a different material. (I suppose if receiveShadow
were a uniform, instead of a #define
, it wouldn't.)
In fact, the following hack causes needsUpdate
to be set once, and two shaders are created.
plane.onBeforeRender = function () { this.material.needsUpdate = true; };
plane.onAfterRender = function () { this.onBeforeRender = function () {}; };
modified fiddle: https://jsfiddle.net/s1nh95y1/4/
This issue can be simplified to the following:
An object that receives shadows cannot share a material with an object that does not receive shadows.
That is obvious if you understand how the #defines
work.
Thing is, .receiveShadow
is a property of an _object_ -- but the property actually applies to the object's material.
Thing is, .receiveShadow is a property of an object -- but the property actually applies to the object's material.
I came to that conclusion sometime ago too.
Is there a good reason not to make this change? I guess it is a significant API change.
Yes. I bumped into this issue when trying to remove skinning
from Material
.
The main problem is that, at the moment, a material can only have one program. But depends of the object, that material may produce one program or another. So, at render time, we check if that material has already compiled a program and we use that one. It works most of the times, but it breaks when the material is used by different types of objects (Mesh
vs SkinnedMesh
) and/or objects with different parameters (.receiveShadow
).
It works most of the times, but it breaks when the material is used by different types of objects (Mesh vs SkinnedMesh) and/or objects with different parameters (.receiveShadow).
I think #12776 is another example of this same issue. (.morphTargets)
Hi I would like to get on this bug. I am new to Three.js but I see this as an opportunity to get knowing it. I tried if this bug is still present and sure it is.
I read a lot that Shading is on the Material side although the recieve and cast Shadow is the property of a Mesh. Also that one Material instance can handle only one Mesh.
How you see the most relevant fix for this issue? Maybe a little notch would help me where to start finding in the code. Maybe modifying the Material implementation to be able handle more Meshes? I really want to hear more opinions on this!!
Maybe on larger project it can make a difference in performace (maybe not).. reusing same Materials and not keeping multiple copies in the memory.
Relating to this workaround which works:
var material1 = new THREE.MeshPhongMaterial();
var material2 = new THREE.MeshPhongMaterial();
sphere = new THREE.Mesh( geometry, material1 );
plane = new THREE.Mesh( geometry2, material2 );
Thanks for help.
PS: As a university student we need to make some contribution on a opensource project. I see that Three.js has an awesome community where I would really love to help.
@WestLangley Is this still a problem?
Yes. I think https://github.com/mrdoob/three.js/issues/11400#issuecomment-304770231 summarizes the issue well.
Hi,
can this be a viable fix?
https://github.com/Chavife/three.js/tree/fix-shadow-cast-bug
It fixed the problem.
maybe PR?
can this be a viable fix?
@Chavife Likely not. Change the sphere to a geometry that will self-shadow and you will see it casts a shadow on itself even if receiveShadow
is false
.
@WestLangley sorry, this option of a state slipped my test... so I learn the hard way...
I think it looks like we need to force a update on a material with a constraint like: "when a program detects that the object is using some material which is already used by an other object than the current one we need a material update"
Hard way test, if this may work is just to set material.needsUpdate
to always true
. This fixes all the states.
Is this looking right? if yes I can do it for a review then.
Or? Do you see some better fix option for it?
this may work ... set material.needsUpdate to always true.
No, that would not be acceptable.
Please realize that experienced three.js developers have not solved this issue in more than a year.
set material.needsUpdate to always true. This fixes all the states.
if this works for you, you are free to do it in your app, I guess? Here, it solves original fiddle:
however, the performance is crap.
I did the implementation anyway just to see if it can work.
https://github.com/Chavife/three.js/tree/fix-shadow-cast-bug
It is basically doing the same as @makc fiddle.
So what I did is just I read @mrdoob comment on this bug
The main problem is that, at the moment, a material can only have one program. But depends of the object, that material may produce one program or another. So, at render time, we check if that material has already compiled a program and we use that one.
So i am just checking in the program creation if there is an another object with the exact same material. If yes then material.needsUpdate
The performace is crap and it looks like this is just and example how not to fix this issue :)
Although IDK if there is an another better solution without significant API change.
I was going to write a new Issue, but this one covers it already:
My project needs a ground with receiveShadow=false and some shapes, casting shadow.
If I use the same material for both, the ground receives shadows anyway.
If I clone the material for the ground, it works fine.
Is the a inconsistence in the logic of three.js?
Three handles receiving shadow in Object3D and its children, not in the material.
But I reckon, WebGL and the GPU handles the shadows in the materials/textures.
If Three loads 3D objects, it will also load the needed materials
and copies the attribute receiveShadow to it.
If true, this is to handle, if it is well documented.
There could be an log-error if different 3D-objects assume different values for shadow to the same material.
Another way would be tho shift the attribute to materials.
If I clone the material for the ground, it works fine.
@DerKarlos Yes. Just do that.
Sure, but ...
I had a hard and long debugging session to find this cross-reaction. My code processes >10k of OpenStreetMap objects. I can't clone that much materials, only if the shadow properties are different. How to explain this to the users of Three.js?
And "shadow" it isn't even mentioned in https://threejs.org/docs/#api/en/constants/Materials :-(
Ok, I could fork and edit and commit it. But I think, we should set up a text togehter, in this thread.
In the real world the shadow-casting works "per material".
Hello, new to ThreeJS but I think I just ran into this issue myself? Or since I'm a newbie with Three I'm doing something wrong? I have a Vue component that renders a fairly basic scene with a plane, two shapes, a spotlight, and I set renderer.shadowMap.Enabled
to true, I have the plane set to receive shadows and the shapes set to cast shadows. I'm not seeing the shadows on the ground though. Thanks in advance for any help!
Here's the script tag from the Vue component:
<script>
import * as THREE from "three";
export default {
data() {
return {
scene: undefined,
camera: undefined,
renderer: undefined
};
},
mounted() {
this.setup();
this.addGeometry();
this.addLight();
this.positionCamera();
this.attachAndRender();
},
methods: {
setup() {
// set up scene, camera, renderer, and axes
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.renderer = new THREE.WebGLRenderer();
this.renderer.setClearColor(new THREE.Color(0x000000));
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.Enabled = true;
},
addGeometry() {
// create plane geometry and material
const planeGeometry = new THREE.PlaneGeometry(60, 20);
const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -0.5 * Math.PI;
plane.position.set(15, 0, 0);
plane.receiveShadow = true;
// create cube geometry and material
const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
const cubeMaterial = new THREE.MeshLambertMaterial({
color: 0xff0000,
wireframe: false
});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(-4, 3, 0);
cube.castShadow = true;
// create sphere geometry and material
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
const sphereMaterial = new THREE.MeshLambertMaterial({
color: 0x7777ff,
wireframe: false
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(20, 4, 2);
sphere.castShadow = true;
// add geometries to the scene
this.scene.add(plane);
this.scene.add(cube);
this.scene.add(sphere);
},
addLight() {
const spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 40, -15);
spotLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
spotLight.shadow.camera.far = 130;
spotLight.shadow.camera.near = 40;
spotLight.castShadow = true;
this.scene.add(spotLight);
},
positionCamera() {
// position the camera for the scene
this.camera.position.set(-30, 40, 30);
this.camera.lookAt(this.scene.position);
},
attachAndRender() {
this.$refs.threeElement.appendChild(this.renderer.domElement);
this.renderer.render(this.scene, this.camera);
}
}
};
</script>
Here's what it looks like in chrome:
Here's what the expected output should look like:
@tetreault Please redirect your help requests to the three.js forum.
this.renderer.shadowMap.enabled = true;
oh wow such a simple stupid mistake... thanks for pointing it out @WestLangley and thanks for the forum link. Will be sure to utilize it!
Most helpful comment
Yes. I bumped into this issue when trying to remove
skinning
fromMaterial
.The main problem is that, at the moment, a material can only have one program. But depends of the object, that material may produce one program or another. So, at render time, we check if that material has already compiled a program and we use that one. It works most of the times, but it breaks when the material is used by different types of objects (
Mesh
vsSkinnedMesh
) and/or objects with different parameters (.receiveShadow
).