Normalized Device Coordinates -- Regarding Integrating Arnold in Blender

Arnold requires two different drivers:

  • One for the display callback (Live buckets rendered to engine.render_result())
  • And one for the output render file for (OpenEXR, TIFF, PNG, etc.)

In order for engine.render_result() to correctly display the rendered image, I need to set the normalized device coordinates for Arnold’s camera. (Otherwise the image appears flipped & upside down)

So here’s where the issue is:

# This results in a correct output file, but then the display callback gets vertically flipped upside down  
arnold.AiNodeSetVec2(node, "screen_window_min", -1, -1)
arnold.AiNodeSetVec2(node, "screen_window_max", 1, 1)
# This results in a correct display callback, but then the output file gets vertically flipped upside down
arnold.AiNodeSetVec2(node, "screen_window_min", -1, 1)
arnold.AiNodeSetVec2(node, "screen_window_max", 1, -1)

Note: node in the above code simply refers to Arnold’s persp_camera

The issue is the results are flip-flopped, how can I get both the callback and the output file to align the same way? Perhaps there is some way to flip Blender’s engine.render_result()?

Also, as a side note, would it be possible to replace the engine.render_result() image with the final output file after all buckets have been rendered, currently it just leaves the display callback’s bucket rendered image.

Thanks!

You can change your display driver code to flip the image, and then use the method that correctly outputs the image file with Arnold’s own drivers.

You can write to the same pixels in the render result as many time as you want.

1 Like

Awesome, thanks! I was able to flip the x and y tiles through the driver code.

The issue though with replacing the engine.render_result() is that Arnold’s output file (exr, png, etc.) is just the output file itself, whereas render_result() expects the pixel data, not the image itself, is there a way to load in the pixel data from an image in a way render_result() can understand?

This is probably the easiest solution:
https://docs.blender.org/api/blender2.8/bpy.types.RenderResult.html?highlight=load_from_file#bpy.types.RenderResult.load_from_file

For best performance / integration you could write an Arnold driver that writes pixels to the Blender render result directly.

1 Like

Brecht! Dude, you’re amazing man. Thank you so much, I really appreciate it.

Awesome. Fixed it, display driver looking great! Thanks brecht for your input:

#include <ai.h>

namespace ASTR {
	static const AtString callback("callback");
	static const AtString callback_data("callback_data");
	static const AtString color_space("color_space");
};

AI_DRIVER_NODE_EXPORT_METHODS(DriverDisplayCallbackMtd)

typedef void(*DisplayCallback)(uint32_t x, uint32_t y, uint32_t width, uint32_t height, float* buffer, void* data);

node_parameters
{
	AiParameterPtr("callback"     , NULL);
	AiParameterPtr("callback_data", NULL);  // This value will be passed directly to the callback function
}

node_initialize
{
	AiDriverInitialize(node, false);
}

node_update
{
}

driver_supports_pixel_type
{
	switch (pixel_type)
	{
	case AI_TYPE_FLOAT:
	case AI_TYPE_RGB:
	case AI_TYPE_RGBA:
		return true;
	default:
		return false;
	}
}

driver_extension
{
	return NULL;
}

driver_open
{
}

driver_needs_bucket
{
	return true;
}

driver_prepare_bucket
{
	DisplayCallback cb = (DisplayCallback)AiNodeGetPtr(node, ASTR::callback);

// Call the callback function with a NULL buffer pointer, to indicate
// a bucket is going to start being rendered.
if (cb)
{
	void *cb_data = AiNodeGetPtr(node, ASTR::callback_data);
	(*cb)(bucket_xo, bucket_yo, bucket_size_x, bucket_size_y, NULL, cb_data);
}
}

driver_write_bucket
{
	int pixel_type;
const void* bucket_data;

// Get the first AOV layer
if (!AiOutputIteratorGetNext(iterator, NULL, &pixel_type, &bucket_data))
return;

const bool dither = true;

// Retrieve color manager for conversion
AtNode* color_manager = (AtNode*)AiNodeGetPtr(AiUniverseGetOptions(), "color_manager");
AtString display_space, linear_space;
AiColorManagerGetDefaults(color_manager, display_space, linear_space);

if (!display_space)
display_space = linear_space;

// Allocates memory for the final pixels in the bucket
//
// This memory is not released here. The client code is
// responsible for its release, which must be done using
// the AiFree() function in the Arnold API
float* buffer = (float*)AiMalloc(bucket_size_x * bucket_size_y * sizeof(float) * 4);
int minx = bucket_xo;
int miny = bucket_yo;
int maxx = bucket_xo + bucket_size_x - 1;
int maxy = bucket_yo + bucket_size_y - 1;

for (int y = 0; y < bucket_size_y; y++)
{
	for (int x = 0; x < bucket_size_x; x++)
	{
		AtRGBA source = AI_RGBA_ZERO;

		switch (pixel_type)
		{
			case AI_TYPE_FLOAT:
			{
				int idx = (bucket_size_y - y - 1) * bucket_size_x + x;
				float f = ((float*)bucket_data)[idx];
				source = AtRGBA(f, f, f, 1.0f);
				break;
			}
			case AI_TYPE_RGB:
			{
				int idx = (bucket_size_y - y - 1) * bucket_size_x + x;
				AtRGB rgb = ((AtRGB*)bucket_data)[idx];
				source = AtRGBA(rgb, 1.0f);
				break;
			}
			case AI_TYPE_RGBA:
			{
				int idx = (bucket_size_y - y - 1) * bucket_size_x + x;
				source = ((AtRGBA*)bucket_data)[idx];
				break;
			}
		}

		AiColorManagerTransform(color_manager, display_space, false, false, NULL, (float*)&source.rgb());

		int i = bucket_xo + x;
		int j = bucket_yo + y;

		float* target = &buffer[(y * bucket_size_x + x) * 4];
		target[0] = source.r;
		target[1] = source.g;
		target[2] = source.b;
		target[3] = source.a;
	}
}

// Sends the buffer with the final pixels to the callback for display.
//
// The callback receives ownership over this buffer, so it must
// release it when it is done with it, using the AiFree() function
// in the Arnold API.
//
// The reason for doing this is to decouple this code from the visualization
// process, so, as soon as the buffer is ready, this driver will send it to
// the callback and return to the rendering process, which will continue
// asynchronously, in parallel with the visualization of the bucket, carried
// out by the client code.
//
DisplayCallback cb = (DisplayCallback)AiNodeGetPtr(node, ASTR::callback);
if (cb)
{
	void *cb_data = AiNodeGetPtr(node, ASTR::callback_data);
	(*cb)(bucket_xo, bucket_yo, bucket_size_x, bucket_size_y, buffer, cb_data);
}
}

driver_process_bucket
{
	// Use this instead of driver_write_bucket for best performance, if your
	// callback handling code is thread safe.
}

driver_close
{
}

node_finish
{
}

node_loader
{
if (i > 0)
return false;

node->methods = DriverDisplayCallbackMtd;
node->name = "driver_display_callback";
node->node_type = AI_NODE_DRIVER;
strcpy(node->version, AI_VERSION);
return true;
}