4
$\begingroup$

It would seem that rotating an image is a trivial task, however I face this problem when working with Multilayer EXR format files in Blender.

But why?

I find it convenient to set my archvis scenes up as animations so I can change objects in different stills and render all camera angles at once as animation. I sometimes need landscape and portrait orientation images of the same interior and use a small script to quickly switch X and Y resolution dimensions and then rotate the camera when I am done framing so some frames render out rotated. It's then very convenient for me to do compositing on a sequence of images.

Recently I rendered a scene in a few batches and ended up with 1 8K panoramic image, 5 4K living room images in landscape orientation with one of them rotated and 5 images of stairs that are all portrait orientation all in a sequence and to my surprise when compositing I can do that with all the images at once as one sequence despite the difference in orientation and resolution and output them to files with File Output nodes to their correct resolution and orientation from that one sequence. So now I know, I can have different orientation in the same sequence which is amasing.

It would be even more amazing if I could always look at all the images rotated correctly while compositing instead of rotating only the output as I have done until now.

It would require me to rotate all the rendered Multilayer EXR files that need rotation in my sequence of files.

What I have tried

I have no problems connecting every output of a Multilayer EXR to File Output node with Python and also inserting any Compositor nodes to edit them automatically so any solution that works for one layer is convenient for me, however I cannot rotate the image in compositor as far because the image is cropped to original dimensions when rotated:

enter image description here

The question

Is it possible to rotate every layer of a Multilayer EXR and output it to an image with rotated dimensions as well with Blender's Compositor nodes, if it is - how?

My solution detailed in and answer below has issues with Cryptomatte. I am starting a bounty in hope to find a solution without those issues. I am starting to think of a solution with Python, but have no idea how it would be simple and easy to use from Blender...

$\endgroup$
  • 2
    $\begingroup$ Does this q/a help? $\endgroup$ – brockmann Nov 18 '20 at 12:18
  • $\begingroup$ I think it does. Thank you! $\endgroup$ – Martynas Žiemys Nov 18 '20 at 12:25
  • $\begingroup$ I figured out a solution with some scaling and then rotating and fitting to render size, but this is so much better! Mask node's dimensions are animatable and I output render with File Output nodes anyway so this can be done while rendering! Thank you for finding this! Animating output dimensions problem was torturing me for years... :D $\endgroup$ – Martynas Žiemys Nov 18 '20 at 12:46
  • $\begingroup$ It took me too long at work to write my answer, didn't see you found a solution. Maybe you can still use it for something. I would have added a blend file as example but I didn't know how. $\endgroup$ – Gordon Brinkmann Nov 18 '20 at 14:58
3
+200
$\begingroup$

About cryptomattes

The issue is not due to the rotation but to the alpha over node:

enter image description here

which results in plain color image.

If you change the node group by this:

enter image description here

That allows to keep the image size from the mask, without considering it (multiplied by 0) but adds the rotated cryptomatte values.

A simple test case file:

$\endgroup$
  • 1
    $\begingroup$ It's great to get an answer that just works. Thank you. Seems to work with color mix nodes as well as long as alpha channel is mixed. I think it might be that Cryptomatte layers just use 4 channels. This is interesting, because Multilayer EXR format layers may have more than 4 channels(see openexr.com/documentation/TechnicalIntroduction.pdf) If cryptomatte layers had more than 4 channels this would not work. Anyway, seems to work for me. $\endgroup$ – Martynas Žiemys Dec 11 '20 at 8:41
  • $\begingroup$ Thanks for the information, @MartynasŽiemys. $\endgroup$ – lemon Dec 11 '20 at 8:46
2
$\begingroup$

This solution has issues - see the end of the answer.

As brockmann pointed out, there is a very similar question about rotating images I failed to find. It sort of explains that image dimensions in the compositor are inherited from the first node if it is mixed with something. Mask node without any mask selected works for setting the dimensions perfectly:

enter image description here

So to rotate a Multilayer EXR file one would need to rotate all the layers and with Python that inside a little addon creating a panel would look something like this:

bl_info = {
    "name": "Nodes",
    "author": "Martynas Žiemys",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "Compositor -> n panel -> Node Tools",
    "description": "",
    "warning": "",
    "doc_url": "",
    "category": "Compositor",
}

import bpy
from bpy.types import Panel, Operator
from mathutils import Vector
from math import radians

class OutputRotatedMultilayer(Operator):
    """Make multilayer EXR file output for all outputs of active image node rotated"""
    bl_idname = "node.multilayer_output_rotated"
    bl_label = "Multilayer Output Rotated"

    @classmethod
    def poll(cls, context):
        image_node_active = False
        if context.scene.node_tree.nodes.active is not None:
            image_node_active = (context.scene.node_tree.nodes.active.type in {"IMAGE","R_LAYERS"})
        return image_node_active

    def execute(self, context):
        if "RotateImage" not in bpy.data.node_groups:
            rotate = bpy.data.node_groups.new('RotateImage', 'CompositorNodeTree')
            rotate.inputs.new('NodeSocketColor','Image')
            rotate.outputs.new('NodeSocketColor','Image')
            out = rotate.nodes.new('NodeGroupOutput')
            out.location = (200,0)
            input = rotate.nodes.new('NodeGroupInput')
            input.location = (-400,0)
            mask = rotate.nodes.new('CompositorNodeMask')
            mask.location = (-200,200)
            mask.use_feather = 0
            mask.size_source = 'FIXED'
            mask.size_x = 3600
            mask.size_y = 2400
            mix = rotate.nodes.new('CompositorNodeAlphaOver')
            mix.location = (0,25)
            mix.hide = 1
            rot = rotate.nodes.new('CompositorNodeRotate')
            rot.location = (-200,-50)
            rot.filter_type = 'NEAREST'
            rot.inputs[1].default_value= radians(90)
            rotate.links.new(out.inputs[0], mix.outputs[0])
            rotate.links.new(rot.inputs[0], input.outputs[0])
            rotate.links.new(mix.inputs[1], mask.outputs[0])
            rotate.links.new(mix.inputs[2], rot.outputs[0])
            
        width = context.scene.node_tree.nodes.active.width
        active = context.scene.node_tree.nodes.active
        tree = context.scene.node_tree
        links = tree.links 
        output = tree.nodes.new('CompositorNodeOutputFile')  
        output.location = active.location + Vector((500,0))
        output.format.file_format = 'OPEN_EXR_MULTILAYER'
        output.format.color_depth = '32'
        output.format.color_mode = 'RGBA'
        output.format.compression = 15
        output.layer_slots.clear()
        for i,every_slot in enumerate(active.outputs):
            if active.type == "R_LAYERS":
                if every_slot.enabled:
                    output.layer_slots.new( name = every_slot.name )
                    g = tree.nodes.new('CompositorNodeGroup')
                    g.node_tree = bpy.data.node_groups["RotateImage"]
                    g.hide = 1 
                    g.location = (-100,i*50)
                    links.new(active.outputs[i], g.inputs[0])
                    links.new(g.outputs[0], output.inputs[every_slot.name])
            else:
                output.layer_slots.new( name = every_slot.name )
                g = tree.nodes.new('CompositorNodeGroup')
                g.node_tree = bpy.data.node_groups["RotateImage"]
                g.hide = 1 
                g.location = active.location + Vector((200,i*-33))
                links.new(active.outputs[i], g.inputs[0])
                links.new(g.outputs[0], output.inputs[every_slot.name])
                
                
        return {'FINISHED'}

class NODE_PT_node_tools(Panel):
    bl_space_type = 'NODE_EDITOR'
    bl_region_type = 'UI'
    bl_category = "Node Tools"
    bl_label = "Node Tools"

    @classmethod
    def poll(cls, context):
        space = context.space_data
        return space.type == 'NODE_EDITOR'

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.operator("node.multilayer_output_rotated")
        
def register():
    bpy.utils.register_class(OutputRotatedMultilayer)
    bpy.utils.register_class(NODE_PT_node_tools)


def unregister():
    bpy.utils.unregister_class(OutputRotatedMultilayer)
    bpy.utils.unregister_class(NODE_PT_node_tools)


if __name__ == "__main__":
    register()

enter image description here

It turns out this does not work for Cryptomatte

The rotation process messes up precision of Cryptomatte values resulting in incorrect mask edges:

enter image description here

$\endgroup$
  • $\begingroup$ If so, please report a bug. Should be possible to rotate a few pixels (mattes). $\endgroup$ – brockmann Dec 9 '20 at 10:03
  • $\begingroup$ Can that be a duplicate of blender.stackexchange.com/questions/143706/… ??? $\endgroup$ – lemon Dec 9 '20 at 10:55
  • $\begingroup$ @lemon I'll investigate pixel filter, thanks. $\endgroup$ – Martynas Žiemys Dec 9 '20 at 11:26
  • $\begingroup$ @MartynasŽiemys, Cryptomatte node, in matte part, the answer from aliasguru. $\endgroup$ – lemon Dec 9 '20 at 11:59
  • $\begingroup$ No, definitely not that, I use Cryptomatte as intended for a long time now and it works as good as it gets on the images that are just rendered without rotation. I still haven't tested the pixel filter settings from the other answer though and that sounds like something that might be related to the problem. $\endgroup$ – Martynas Žiemys Dec 9 '20 at 12:28
1
$\begingroup$

Since the Render Result doesn't take the dimensions or orientation of the EXR into account, the answer depends on what result you want to achieve. The simple way would be, if the EXR is for example 1920 x 1080, set the Render Dimensions to 1920 x 1920. This way the image will always fit and not be cropped either way. If you want to render at a different percentage like 50% maybe, you should place a Scale Node after the Rotate Node set to Render Size > Fit.

If you want to keep the aspect ratio of the rendered image at 16:9, then there are two possible ways to go:

  1. The EXR should fill as much space as possible (1920 x 1080 at 0°, 608 x 1080 at 90°) or
  2. the dimensions should stay the same (1080 x 608 at 0°, 608 x 1080 at 90°).

Either way you need the Scale Node described above with Render Size > Fit and a second Scale Node set to Relative > X = Y = 9/16 = 0.5625.

For the option 1 you have to disable the Relative node or set X = Y = 1 if the rotation is 0°. For 90° or -90° you have to enable it with X = Y = 0.5625.

For option 2 you just leave it enabled no matter if 0° or 90°, but as mentioned before in this case the EXR will not fill the image horizontally.

I've created a node setup with automatic disabling of the Relative node according to the Rotation value, see image. The idea is if you want to render a sequence and set keyframes for the rotation, you don't have to manually disable nodes. The aspect ratio of the Render Dimensions is calculated by drivers.

option 1

option 2

$\endgroup$
  • $\begingroup$ Thanks for the time put into this, but it seems you only described two problem cases. I want to take an image with some dimensions and keep them when rotated so full rotated image should have those exact dimensions only switched(exactly the same thing that we just call rotating an image in all other software). Making the image fit inside a bigger image is no good. These are large files and having extra black space is an issue. But I have a very good solution already based on the answer brockmann linked to explaining that dimensions are inherited from the first node mixed with anything. $\endgroup$ – Martynas Žiemys Nov 18 '20 at 15:36
  • $\begingroup$ Well, option 2 keeps the image dimensions the same vertical and horizontal - the scale depends on the scene size. But it seems I didn't understand your question or I don't understand the proposed solution. The mask helps me to view the image uncropped in the Compositor, but I thought you wanted to render it out in a sequence or animation. When I render two images in a sequence, one at 0° and one at 90° with switched dimensions, the rotated one is still cropped and has empty space left and right. If you worked out a different solution based on the tip you could give it as answer here for all. $\endgroup$ – Gordon Brinkmann Nov 19 '20 at 7:03
  • $\begingroup$ Posted the solution I ended up with. Obviously, I will just output the render result rotated from now on, but ability to rotate Multilayer EXR is useful as well. $\endgroup$ – Martynas Žiemys Nov 19 '20 at 10:09
  • $\begingroup$ I guess one of the problems with rendering out switched orientation in a sequence is that the render output is always bound to the scene resolution and its scaling. Since an animation could not only be rendered as an image sequence but directly to a video file, switching dimensions and orientation is not possible because a video file wouldn't allow that. $\endgroup$ – Gordon Brinkmann Nov 19 '20 at 10:16
  • 1
    $\begingroup$ It works when outputting with File Output nodes from the Compositor so I don't care about the regular render output. It's a pity actual render resolution cannot be changed during animation rendering. $\endgroup$ – Martynas Žiemys Nov 19 '20 at 11:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.