Undo method for LayerShaderLayer
-
Hey @John_Do,
Thank you for reaching out to us. A
LayerShaderLayer
is indeed no scene element (BaseList2D
). The type is just a convenience interface into the layer data stored in the data container of aLayerShader
. You therefore must add the undo's to theLayerShader
, even for creation and deletion events.Please provide executable code in future threads, linking to old code which does not contain the relevant problem is not sufficient. I have provided a solution below. The thread "How to create and populate a layer shader" might also be helpful in your context.
Cheers,
FerdinandCode:
"""Demonstrates how to define undo's for a layer shader. The layers of a layer shader (LayerShaderLayer instances) are just smoke an mirrors, all the data is stored in the data container of the layer shader itself. Undo's for creating, deleting, and modifying layer shader layers are therefore always data container undo's, i.e., CHANGE_SMALL. Running this script will create a material with a layer shader in its color channel. The material will have two undo steps; one for the creation of the material and the layer shader, and one for the manipulation of the layer shader. See also: https://developers.maxon.net/forum/topic/14059/how-to-create-and-populate-a-layer-shader/2 """ import c4d from mxutils import * doc: c4d.documents.BaseDocument # The active document def main() -> None: """ """ # Create a material, a layer shader, and a noise shader we are going to use in the layer shader. material: c4d.BaseMaterial = CheckType(c4d.BaseList2D(c4d.Mmaterial)) layerShader: c4d.BaseShader = CheckType(c4d.LayerShader()) noiseShader: c4d.BaseShader = CheckType(c4d.BaseList2D(c4d.Xnoise)) # Link the shaders and set the layer shader as the color channel of the material. material.InsertShader(layerShader) noiseShader.InsertUnder(layerShader) material[c4d.MATERIAL_COLOR_SHADER] = layerShader # Add one undo step for adding the material. We do not extra undo's for adding the layer shader # attached to the material or the noise shader attached to the layer shader just as we would not # add multiple undo's when adding a hierarchy of geometry objects. doc.StartUndo() doc.InsertMaterial(material) doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, material) doc.EndUndo() doc.SetActiveMaterial(material) # Now we start a second undo to collect under this all changes to the layer shader. Doing this # is not really necessary, we just do this to demonstrate the different nature of such undo # steps. doc.StartUndo() # Although we are calling the method LayerShader.AddLayer there is nothing really added to the # scene. A LayerShaderLayer is just a convenance interface for its LayerShader. All layer data # is stored in the data container of the layer shader. Adding, removing or modifying layers are # therefore all data container operations on the layer shader. # We summarize all four following operations (add two layers, change a parameter on each layer) # in one UNDOTYPE_CHANGE_SMALL undo. doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, layerShader) layerHsl: c4d.LayerShaderLayer = CheckType(layerShader.AddLayer(c4d.TypeHSL)) layerNoise: c4d.LayerShaderLayer = CheckType(layerShader.AddLayer(c4d.TypeShader)) layerHsl.SetParameter(c4d.LAYER_S_PARAM_HSL_LIGHTNESS, -.5) layerNoise.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, noiseShader) doc.EndUndo() # Push and update event to Cinema 4D. c4d.EventAdd() if __name__ == '__main__': main()
-
Hi @ferdinand,
Thank you for the sample code ! Indeed I did not provide my code to avoid a complicated request in case I would have missed something really obvious.
I guessed that your answer was the way to go and already tried it before creating the topic, unfortunately it doesn't work. The good news is it should, so something else must be wrong in my code. You'll find the problematic part below, the part with the Layer Shader layer is the first case in the convert_c4d_bitmap() function :
def get_layershader_layer(doc: c4d.documents.BaseDocument, layer_shader: c4d.BaseShader, target_shader: c4d.BaseShader = None) -> c4d.LayerShaderLayer: # Iterating over Layer Shader layers, looking # for the specified type of shaders if layer_shader.GetType() == c4d.Xlayer: layer = layer_shader.GetFirstLayer() while layer != None: if layer.GetType() == c4d.TypeShader: shader = layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) if target_shader and shader == target_shader: return layer layer = layer.GetNext() elif layer.GetNext(): layer = layer.GetNext() else: layer = None def convert_c4d_bitmap(doc: c4d.documents.BaseDocument, c4d_bitmap: c4d.BaseShader, remove=True): cr_bitmap = c4d.BaseShader(1036473) cr_bitmap_added = True if c4d_bitmap.GetUp(): parent = c4d_bitmap.GetUp() cr_bitmap.InsertUnder(parent) doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap) # if parent is a layer shader if parent.CheckType(c4d.Xlayer): layer = get_layershader_layer(doc, parent, c4d_bitmap) cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc) doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent) layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap) # if parent is another type shader (i.e. Fusion Shader) else: parent_shader_slot = get_bc_id( parent.GetDataInstance(), c4d_bitmap) doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) parent[parent_shader_slot] = cr_bitmap # if parent is a material elif c4d_bitmap.GetMain(): parent = c4d_bitmap.GetMain() doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) parent.InsertShader(cr_bitmap) doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap) parent_shader_slot = get_bc_id(parent.GetDataInstance(), c4d_bitmap) parent[parent_shader_slot] = cr_bitmap else: cr_bitmap_added = False # Removing the old bitmap if remove and cr_bitmap_added: doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, c4d_bitmap) c4d_bitmap.Remove() return cr_bitmap def get_bc_id(target_bc, target_value): for index, value in target_bc: if value == target_value: return index
Thank you
-
Hey @John_Do,
This is unfortunately not executable code but just a snippet, and I am always very hesitant to make claims about code I cannot run. The part which is not working I assume is this?
# if parent is a layer shader if parent.CheckType(c4d.Xlayer): layer = get_layershader_layer(doc, parent, c4d_bitmap) cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc) doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent) layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap)
The code looks correct. What springs to eye is that there is no
StartUndo
andEndUndo
in your code. I assume they are outside of this snippet because you batch-process multiple materials. What also springs to eye is the cr_bitmap, which I assume stands for Corona bitmap. Have you tried what happens when replace all Corona materials/shaders with standard renderer ones? When I understand your code correctly, you are trying to link a Corona bitmap in a Standard renderer layer shader.You also type hinted your
get_layershader_layer
and treat its output as if it would be alwaysLayerShaderLayer
but its return type is actuallyLayerShaderLayer | None
. Are you sure that there is no error in your code wherelayer is None
and you try to treat it otherwise? You could also print out the return value of thatSetParameter
call.Cheers,
Ferdinand -
Hey @ferdinand ,
Sorry, full code below !
layer.SetParameter(...) returns True for each call as the assignation works. The code did not raise any error or exception. But if I undo/redo, bitmap shaders disappears, Corona ones as well as the previous Cinema ones. It seems that only Layer Shader's layers remain, but with a missing/broken shader link ?
# CT_C4DBitmapToCoronaBitmap import c4d def iter_shaders(material: c4d.BaseMaterial, mask: int = None) -> list: shaders = [] shader = material.GetFirstShader() if shader == None: return while shader: if mask: if shader.GetType() == mask: shaders.append(shader) else: shaders.append(shader) if shader.GetDown(): shader = shader.GetDown() elif shader.GetNext(): shader = shader.GetNext() else: while shader.GetUp(): if shader.GetUp().GetNext(): shader = shader.GetUp().GetNext() break shader = shader.GetUp() else: shader = None return shaders def get_layershader_layer(doc: c4d.documents.BaseDocument, layer_shader: c4d.BaseShader, target_shader: c4d.BaseShader = None) -> c4d.LayerShaderLayer: # Iterating over Layer Shader layers, looking # for the specified type of shaders if layer_shader.GetType() == c4d.Xlayer: layer = layer_shader.GetFirstLayer() while layer != None: if layer.GetType() == c4d.TypeShader: shader = layer.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) if target_shader and shader == target_shader: return layer layer = layer.GetNext() elif layer.GetNext(): layer = layer.GetNext() else: layer = None def convert_c4d_bitmap(doc: c4d.documents.BaseDocument, c4d_bitmap: c4d.BaseShader, remove=True): cr_bitmap = create_cr_bitmap_shader( c4d_bitmap[c4d.BITMAPSHADER_FILENAME], c4d_bitmap[c4d.BITMAPSHADER_COLORPROFILE], c4d_bitmap[c4d.ID_BASELIST_NAME], c4d_bitmap[c4d.BITMAPSHADER_EXPOSURE], c4d_bitmap) cr_bitmap_added = True if c4d_bitmap.GetUp(): parent = c4d_bitmap.GetUp() cr_bitmap.InsertUnder(parent) doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap) if parent.CheckType(c4d.Xlayer): layer = get_layershader_layer(doc, parent, c4d_bitmap) cr_bitmap[c4d.ID_BASELIST_NAME] = layer.GetName(doc) doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, parent) layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, cr_bitmap) else: parent_shader_slot = get_bc_id( parent.GetDataInstance(), c4d_bitmap) doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) parent[parent_shader_slot] = cr_bitmap elif c4d_bitmap.GetMain(): parent = c4d_bitmap.GetMain() doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) parent.InsertShader(cr_bitmap) doc.AddUndo(c4d.UNDOTYPE_NEW, cr_bitmap) parent_shader_slot = get_bc_id(parent.GetDataInstance(), c4d_bitmap) parent[parent_shader_slot] = cr_bitmap else: cr_bitmap_added = False # Removing the old bitmap if remove and cr_bitmap_added: doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, c4d_bitmap) c4d_bitmap.Remove() return cr_bitmap def create_cr_bitmap_shader(filepath: str, colorspc: int, basename: str, exposure: float, shader): cr_bitmap = c4d.BaseShader(1036473) cr_bitmap_bc = cr_bitmap.GetDataInstance() cr_bitmap_bc.SetFilename(c4d.CORONA_BITMAP_FILENAME, filepath) cr_bitmap_bc.SetInt32(c4d.CORONA_BITMAP_COLORPROFILE, colorspc) cr_bitmap_bc.SetString(c4d.ID_BASELIST_NAME, basename) cr_bitmap_bc.SetFloat(c4d.CORONA_BITMAP_EXPOSURE, exposure if colorspc in [1, 3] else exposure * 2.2) return cr_bitmap def get_bc_id(target_bc, target_value): for index, value in target_bc: if value == target_value: return index def main(): doc = c4d.documents.GetActiveDocument() materials = doc.GetActiveMaterials() doc.StartUndo() # Main loop for material in materials: # Step over non Corona Physical materials or Corona Legacy materials. if material.GetType() not in (1056306, 1032100): continue for bitmap_shader in iter_shaders(material, c4d.Xbitmap): convert_c4d_bitmap(doc, bitmap_shader) doc.EndUndo() c4d.EventAdd() if __name__ == '__main__': main()
-
Hey @John_Do,
Thank you for the full code. I did what I proposed above and removed Corona from the equation, replacing the corona bitmap shader with the standard one. The problem remains the same, so this is not a problem of Corona. I then went ahead and wrote a clean version of your script, removing all the fluff, but the problem remains. Once one replaces a shader layer in LayerShader, wrapping the action in an undo, the resulting undo stack will not go back to the original shader but an "empty" layer.
Fig. 1: Top to bottom - (1) Initial state of the LayerShader, (2) after the script ran, (3) after pressing undo once, the shader is empty.Even though we correctly establish an undo-item for removing the original shader and setting the parameter, going back in the stack then seems to break the linkage. I will try to take a closer look at our code base this week, in the hopes of finding some magic sauce we are missing here.
But your code is more or less correct. I also tried deliberately different orders of adding the new shader and removing the old one on the stack, with no luck. So, you should not look for a solution, as the solution is either very fringe or there is a bug/limitation in the LayerShader undo handling.
Cheers,
FerdinandCode:
# CT_C4DBitmapToCoronaBitmap import c4d import typing from mxutils import * doc: c4d.documents.BaseDocument # The active document. def iter_shaders(owner: c4d.BaseList2D, node: c4d.BaseList2D, mask: list[int] | None = None) -> typing.Iterator[tuple[c4d.BaseList2D, c4d.BaseShader]]: """ """ if owner is None or node is None: return CheckType(owner, c4d.BaseList2D) CheckType(node, c4d.BaseList2D) mask = [] if mask is None else CheckIterable(mask, int, list) while node: if isinstance(node, c4d.BaseShader) and node.GetType() in mask: yield owner, node for data in iter_shaders(node, node.GetFirstShader(), mask): yield data for data in iter_shaders(node, node.GetDown(), mask): yield data node = node.GetNext() def replace_bitmap_shaders(material: c4d.BaseMaterial) -> None: """ """ def get_layer( layers: c4d.LayerShader, shader: c4d.BaseShader) -> c4d.LayerShaderLayer: """ """ CheckType(shader, c4d.BaseShader) current: c4d.LayerShaderLayer = CheckType(layers, c4d.LayerShader).GetFirstLayer() while current: if (current.GetType() == c4d.TypeShader and current.GetParameter(c4d.LAYER_S_PARAM_SHADER_LINK) == shader): return current current = current.GetNext() raise RuntimeError(f"Could not find layer for {shader} in {layers}.") CheckType(material, c4d.BaseMaterial) for owner, shader in iter_shaders(doc, material, mask=[c4d.Xbitmap]): replacement: c4d.BaseShader = create_standard_bitmap_shader(shader) replacement.InsertUnderLast(owner) doc.AddUndo(c4d.UNDOTYPE_NEW, replacement) if isinstance(owner, c4d.LayerShader): layer: c4d.LayerShaderLayer = get_layer(owner, shader) doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, owner) layer.SetParameter(c4d.LAYER_S_PARAM_SHADER_LINK, replacement) shader.Remove() doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, shader) def create_standard_bitmap_shader(shader: c4d.BaseShader) -> c4d.BaseShader: """ """ copy: c4d.BaseShader = CheckType(shader.GetClone(c4d.COPYFLAGS_NONE)) copy[c4d.BITMAPSHADER_FILENAME] = "asset:///file_fa9c42774dd05049" return copy def main(): """ """ doc.StartUndo() for material in doc.GetActiveMaterials(): replace_bitmap_shaders(material) doc.EndUndo() c4d.EventAdd() if __name__ == '__main__': main()
-
Hi @ferdinand ,
Thank you so much for taking the time to test all of this. Out of curiosity I tried your code on my side, and the same thing happen on native Cinema 4D materials.
Please let me know if you find the root cause of the bug, in the meantime I'll publish the script with all necessary warnings.
Have a nice day
-
Hey @John_Do,
Yes, I only ran this with Cinema 4D Standard Renderer shaders, so the problem is definitively with our code. And I will certainly report back here when I have a more concrete answer.
As a little warning, other than the options (1) "one must do a weird thing" and (2) "this is a bug", it could also be that we declare this an intended or accepted limitation of the Python layer. The interfaces of
LayerShader
are identical in Python and C++, but that does not mean that they did not take a shortcut under the hood.I will have a look within this week, for now I have moved this topic to the bug forum.
Cheers,
Ferdinand -
-
Hi @ferdinand , have you got a chance to look at the issue from the Cinema 4D side ?
-
Hey @John_Do,
please excuse the wait, I now had a look. Unfortunately, we must classify this as an accepted/known limitation and I (at least currently) also cannot offer a workaround.
In the public Python and C++ API we have the types
LayerShader
andLayerShaderLayer
. As with most types, we have internal counterparts for them, a backend in our private API, where the actual implementation happens. The problem is here, that this backend is barely used by ourselves, and internally we actually use a shader and layer class that comes directly from the legacy Smells Like Almonds (SLA) plugin system Maxon once bought and the whole layer shader came from. When a user interacts with the LayerShader custom GUI, that custom GUI actually uses these SLA types and notLayerShader
andLayerShaderLayer
. So, when a user is adding a layer to a LayerShader in the GUI,LayerShader::AddLayer
is for example never called, as we use the a method on the internal SLA type instead.I do not fully understand myself why the
BaseLink
is broken in this very special case, but since we operate here with this 'double structure', it would be way too much work to fix. We have therefore also declared this an accepted limitation.Due to the fact that we operate here with a type which is not really used by ourselves, I also do not see a workaround for you.
Cheers,
Ferdinand -
Oof, that's an unfortunate end for me but at least I have a clear explanation. Thanks @ferdinand