mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
101.59k stars 35.29k forks source link

Please allow each `Material` to turn on/off shadow casting #27564

Closed finnbear closed 7 months ago

finnbear commented 7 months ago

Description

Please make Material able to control shadow casting behavior (in addition to or instead of Object3D), considering that a single Object3D may have multiple materials with different needs.

If I understand correctly, each material/group is rendered in a separate draw call, so they could be filtered during shadow rendering.

While I don't personally need each material to also control whether to receive shadows, it could be a logical addition to this proposal.

Solution

Material.castShadow = false; // default false
Material.receiveShadow = true; // default false
// Deprecated
Object3D.castShadow = true;
// Deprecated
Object3D.receiveShadow = true;

Downside: harder to reuse same material if some uses need shadows and others don't.

Alternatives

Material.castShadow = null; // use Object3D.castShadow
Material.castShadow = true | false; // override
Material.receiveShadow = null; // use Object3D.receiveShadow
Material.receiveShadow = true | false; // override

or (only for shadow casting)

// Existing property set to new value
Material.shadowSide = THREE.NeitherSide;

or (also only for shadow casting, specifically windows)

Material.shadowAlphaTest = 0.5; // default 0.0

or (this doesn't work right now for some reason)

Object3D.onBeforeShadow = () => {
  Material.visible = false;
};
Object3D.onAfterShadow = () => {
  Material.visible = true;
};

or (thought of by Ghost)

BufferGeometry.groups[n].castShadow = false;

or (thought of by WestLangley) https://github.com/mrdoob/three.js/issues/27564#issuecomment-1892715348

Additional context

I have a multi-group, multi-material Mesh and one of the groups/materials is windows/transparent glass. I would like that one material to not cast shadows. How can I accomplish this? (castShadows is a property of Object3D, not Material). Splitting into multiple objects would be annoying.

I asked for advice on Discord but received no responses. I'm open to attempting workarounds that don't require splitting up my Mesh into multiple.

image

RemusMar commented 7 months ago

I have a multi-group, multi-material Mesh and one of the groups/materials is windows/transparent glass. I would like that one material to not cast shadows. How can I accomplish this?

Are you aware of the ShadowMaterial? https://threejs.org/docs/#api/en/materials/ShadowMaterial

finnbear commented 7 months ago

Are you aware of the ShadowMaterial?

While I meant partially transparent, at first ShadowMaterial seemed promising as a partial solution for glass (to make it slightly visible but not cast shadows). However, it appears that even ShadowMaterial casts shadows. It also visually disappears when the shadow map lacks data. image

WestLangley commented 7 months ago

Try mesh.onBeforeRender( renderer, scene, camera, geometry, material ) and set .castShadow to false for the desired material only. Restore with .onAfterRender().

finnbear commented 7 months ago

Try mesh.onBeforeRender( renderer, scene, camera, geometry, material ) and set .castShadow to false for the desired material only. Restore with .onAfterRender().

Thanks @WestLangley! I tried this:

mesh.onBeforeRender = (renderer, scene, camera, geometry, material) => {
    console.log(geometry, material);
    if (material.transparent) {
      mesh.castShadow = false;
    }
  };
  mesh.onAfterRender = (renderer, scene, camera, geometry, material) => {
    if (material.transparent) {
      mesh.castShadow = true;
    }
  };

console.log confirmed the code (including mesh.castShadow = false; ran properly, but I observed no visual difference.

WestLangley commented 7 months ago

It is best if you provide a live example so we can avoid guessing... Is your material both transparent and double-sided?

If so, try adding material.forceSinglePass = true when the material is instantiated.

finnbear commented 7 months ago

It is best if you provide a live example so we can avoid guessing...

@WestLangley Sorry, here you go: https://playcode.io/1725402 Edit: https://playcode.io/1725402 image image

Edit: I updated the example to toggle materials every 2s, so it is clear which one(s) cast shadows on the other(s)

Is your material both transparent and double-sided

My window material is transparent but none are double-sided (as in THREE.BothSides). However my geometry is double-sided (as in solid volume, for physics reasons).

WestLangley commented 7 months ago

Unfortunately, the suggested workarounds are not working for me, either.

Mugen87 commented 7 months ago

considering that a single Object3D may have multiple materials with different needs.

Something that definitely works is to use SceneUtils.createMeshesFromMultiMaterialMesh() which splits up a multi-material mesh into separate instances of Mesh. You can then configure the shadow properties as usual.

As mentioned above, there is no performance difference since multi-material meshes ends up as separate render items anyway.

Splitting into multiple objects would be annoying.

At least you have a routine as a workaround. If you would use GLTFLoader, you would never get meshes with group data defined. The BufferGeoemtry.groups/multi-material API is somewhat controversial and we have already thought about alternatives.

finnbear commented 7 months ago

Something that definitely works is to use SceneUtils.createMeshesFromMultiMaterialMesh() which splits up a multi-material mesh into separate instances of Mesh. You can then configure the shadow properties as usual.

Thanks for linking to that API, which would definitely make a workaround easier. I might use it for the time being. Does require code to keep the physics using a single mesh, but that shouldn't be that hard.

The BufferGeoemtry.groups/multi-material API is somewhat controversial and we have already thought about alternatives.

I personally like it. Makes my life easier (except for this issue). Curious if you have a link to discussions about replacing it.

RemusMar commented 7 months ago

It also depends on your workflow to generate those 3D models. I'm using 3DS Max and the BabylonJS exporter and the results are always the best: a mesh with multiple materials is exported as a group with multiple meshes (a single material per mesh). So in this case, to get what you're looking for is clean and fast. Example: https://necromanthus.com/Test/html5/Glass.html

            myBox.traverse( function ( child ) {
                if ( child.isMesh ) {
                    if ( child.material.name != "glass") {
                        child.castShadow = true;
                        child.receiveShadow = true;
                    }
                }
            });
Mugen87 commented 7 months ago

I personally like it. Makes my life easier (except for this issue). Curious if you have a link to discussions about replacing it.

The discussion is unfortunately scattered around different issues and PRs.

However, there is no concrete plan to remove groups/multi-materials. But maybe implement it differently. Other engines and DCC tools support similar concepts and certain developers rely on it.

finnbear commented 7 months ago

It also depends on your workflow to generate those 3D models.

I'm well aware of the ability of my workflow (obj for now, but also gltf) to export different meshes for each material. I intentionally avoided doing that so my scene would be simpler. I might switch to doing that as a workaround, but not a full solution to this issue.

Mugen87 commented 7 months ago

Since this issue is all about shadows of transparent objects:

Would you still need more fain-grained shadow configuration if #15999 would be merged? This PR would make sure shadows of transparent objects look better.

Mugen87 commented 7 months ago

Conceptually, turning on/off shadows is something that you ideally want to configure per object. Simply because from a physically correct point of view it is not plausible when only parts of an object cast (or receive) shadows.

IMO, it makes more sense to improve the shadow quality instead so devs do not want to turn of shadows because the result doesn't look good. Hopefully https://github.com/mrdoob/three.js/pull/15999 is a first step towards this goal.

RemusMar commented 7 months ago

I'm with you Michael, In the real world only an object (mesh) can generate a shadow, not a material (which is an object property). In my opinion the best solution here is to keep the current logic and to add the real missing feature: the shadow intensity depends on the material opacity.

finnbear commented 7 months ago

the shadow intensity depends on the material opacity

I must say, that's a much better solution (didn't know it was possible) :smile:

Will close this for now.