Open OndrejSpanel opened 1 year ago
I ran into a similar problem recently, and would like to share the use case here. Consider an editor-like application using THREE.GridHelper, and offering different viewport shading options as Blender does:
I'd thought this would be easy:
// 1. Rendered — use original material
scene.overrideMaterial = null;
// 2. Solid — use matcap material
scene.overrideMaterial = new MeshMatcapMaterial({ flatShading: true });
// 3. Wireframe — use wireframe material
scene.overrideMaterial = new MeshBasicMaterial({ wireframe: true });
However, (3) breaks GridHelper with errors like...
[.WebGL-0x1280a426900] GL_INVALID_OPERATION: Vertex buffer is not big enough for the draw call
... probably because it's a LineSegments object and not a mesh. And ideally I'd exclude the editor's own UI, like helpers, from the override. Something like a mapping function would certainly work here. Or perhaps a mapping based on Layers?
I'm unsure whether this is something we should have an API for... would callbacks on every material add too much overhead? Interested what others think here.
Tangentially related:
Or perhaps a mapping based on Layers?
Maybe overrideMaterial
could be made an array, one material for each layer?
We could borrow a trick from CSS, letting any material override the .overrideMaterial
...
scene.overrideMaterial = new MeshMatcapMaterial({ flatShading: true });
// ...
gridHelper.material.important = true;
😱
I'm worried though, that we would be taking something the user can do in a one-time traverse()
to replace materials...
const originals = new Map();
// override
scene.traverse((object) => {
if (!object.material) return;
const material = object.material;
originals.set(object, material);
object.material = overrideMaterial;
});
// reset
for (const [object, material] of originals.entries()) {
object.material = material;
}
... and turning that into a one-time-per-frame operation in the engine. We probably do not want that. Though it is really nice to be able to override the material without reassigning it, e.g. in an editor where the user may change an objects materials while in a wireframe-only view.
... and turning that into a one-time-per-frame operation in the engine.
The operation is already there, in the renderObjects
the renderer is doing:
const material = overrideMaterial === null ? renderItem.material : overrideMaterial;
It would change to:
const material = scene.mapMaterial(renderItem.material, renderItem.object)
Scene.mapMaterial
would be initialized in the Scene
constructor as:
this.mapMaterial = (material) => this.overrideMaterial === null ? material : this.overrideMaterial
It is a small change, making material override much more flexible, suitable for implementing custom shadow map rendering, env, map rendering and similiar tasks.
Would you accept a PR along the lines suggested by @OndrejSpanel in the last comment? Having an overridable function in Scene
seems like a good way to implement this.
How do you restore the original implementation of a callback after the override? Now you can do this:
scene.overrideMaterial = materialNormal;
renderer.setRenderTarget( normalRenderTarget );
renderer.clear();
renderer.render( scene, camera );
renderer.setRenderTarget( null );
scene.overrideMaterial = null;
With a callback it would be:
scene.mapMaterial = applyCustomOverride;
renderer.setRenderTarget( normalRenderTarget );
renderer.clear();
renderer.render( scene, camera );
renderer.setRenderTarget( null );
scene.mapMaterial = ???; // null does not work here
Besides, I would rename mapMaterial
to something like onMaterialOverride()
.
const material = scene.mapMaterial(renderItem.material, renderItem.object)
This code in WebGLRenderer
does not work since scene
can be of type Mesh
or any other 3D object. You have to move mapMaterial()
to Object3D
or check if the isScene
flag is true
. The latter should look like so using onMaterialOverride()
:
const overrideMaterial = scene.isScene === true ? scene.onMaterialOverride( object ) : null;
const material = overrideMaterial === null ? renderItem.material : overrideMaterial;
The default implementation in Scene
is:
onMaterialOverride( /* object = null */ ) {
return this.overrideMaterial;
}
However, the question remains how you toggle the override.
One approach is to use this pattern know from other parts of the code base:
const oldMaterialOverride = scene.onMaterialOverride;
scene.onMaterialOverride = applyCustomOverride;
// restore
scene.onMaterialOverride = oldMaterialOverride;
I wonder if there is a nicer API though...
Description
I want to render my scene for reflections. For that I want to replace all materials with simpler variants. Currently the only possible override is
scene.overrideMaterial
, but sometimes the materials in scene are so different it is hard to replace them with one material only.Solution
I suggest extending this by adding a new member -
scene.mapMaterial
, which would be a function receiving material and returning material, allowing to map any material and replacing is as desired (often one could implement it using a simple hashmap holding alternative representations of the material).Alternatives
One possible alternative is to clone the whole scene and replace the materials on all objects. Another alternative is to control material detail using material defines or uniforms, but that requires updating all materials at least twice in each frame.
Additional context
To avoid additional tests, current functionality of
overrideMaterial === null ? renderItem.material : overrideMaterial
would be handled by a default value ofmapMaterial
, containing this code wrapped in a function.The solution would allow functionality similar to what is currently done for shadow maps in
getDepthMaterial
function.If the idea seems reasonable, I will be glad to prepare PR.