3
$\begingroup$

I made a simple material that uses the 'object info' location to drive the color of the object. But when i animate my object now it, obviously, changes color while the objects moves through space. How can i set the color to a fixed value using the INITIAL location of the Object info node? My node setup Tnx RDG

$\endgroup$
  • 1
    $\begingroup$ Are the objects particles? If yes you are out of luck. If not and they are objects there is a way. $\endgroup$ – Jaroslav Jerryno Novotny Jan 24 '17 at 10:48
5
$\begingroup$

To achieve this you essentially need to be able to do two things :

  1. Capture the location of each object at a point in time and store it
  2. Access the stored data efficiently from within the Cycles material

The first task is relatively straight forward using a Python script to capture the object's location as follows :

import bpy

num_objects = len(bpy.data.objects)

for x in range(0,num_objects):
    object = bpy.data.objects[x]
    object["initial_location"] = [object.location[0], object.location[1], object.location[2]]

This script runs through each object in turn, determines its location, and stores it in a Custom Property on each object.

stored location

While this is useful to demonstrate the capture of a property of each object, the Custom Properties aren't the best place to store the details since Cycles materials cannot (easily or efficiently) access the contents of the properties. Future Cycles enhancements may well provide a mechanism for reading object properties but for now we need a different method - we can use an Image Texture.

Cycles can very efficiently read the value of a pixel within an image using the Image Texture node and this provides an ideal method of passing the data into the material - encoded as the color of each pixel within an image. A normal 8-bit RGB image will not be sufficient - so we need to use a format that allows full 32-bit floating point values to allow it to hold the location vectors.

The following script will take the location of each object (as in the previous script) and will create an image 1 pixel high and wide enough to hold one pixel per object. Each pixel will be set with its RGB values set to the XYZ vector of the location of the corresponding object.

import bpy

width = len(bpy.data.objects)
height = 1

# Create the image
image = bpy.data.images.new("object_locations_"+str(width), width=width, height=height,float_buffer=True)

# Create an empty array of pixel data (each will hold R, G, B, A values as floats)
pixels = [None] * width * height

for x in range(0,width):
    y = 0
    object = bpy.data.objects[x]
    pixels[(y*width)+x] = [object.location[0], object.location[1], object.location[2],1.0]
    object.pass_index = x

# 'flatten' the array - so [R1,G1,B1,A1], [R2,G2,B2,A2], [R3,G3,B3,A3],.... becomes R1,B1,G1,A1,R2,G2,B2,A2,R3,G3,B3,A3,....    
pixels = [chan for px in pixels for chan in px]

# Store the pixels in the image
image.pixels = pixels
image.update()

The script runs through each object and captures the location in a pixel in the image. Note that it also sets the object's 'pass_index' to the 'slot' corresponding to that object's pixel in the image - this will be used in the material to retrieve the correct pixel for each object. Running the script on my test scene produced the following image. Dragging the mouse over the pixels shows that each pixel relates to the XYZ location of each object in the scene.

captured pixels

The image needs to be saved - either as an image file or 'packed' into the blend file. However, this causes a potential problem. 'Normal' images are stored as a set of R, G, B, A values in the range 0.0 to 1.0 but our object locations will likely be well outside that range. To prevent this corrupting our stored values we need to ensure it is saved in a format that can store full float values (rather than a PNG). Saving and packing the image as a 32-bit float image is covered in this question. This produces the following final script to capture all object locations into a 32-bit float packed image :

import bpy
import os

width = len(bpy.data.objects)
height = 1

# Create the image
image = bpy.data.images.new("object_locations_"+str(width), width=width, height=height,float_buffer=True)

# Create an empty array of pixel data (each will hold R, G, B, A values as floats)
pixels = [None] * width * height

for x in range(0,width):
    y = 0
    object = bpy.data.objects[x]
    pixels[(y*width)+x] = [object.location[0], object.location[1], object.location[2],1.0]
    object.pass_index = x

# 'flatten' the array - so [R1,G1,B1,A1], [R2,G2,B2,A2], [R3,G3,B3,A3],.... becomes R1,B1,G1,A1,R2,G2,B2,A2,R3,G3,B3,A3,....    
pixels = [chan for px in pixels for chan in px]

# Store the pixels in the image
image.pixels = pixels
image.update()

# Save image to temporary file and pack it (from answer by @sambler)
scn = bpy.data.scenes.new('img_settings')
scn.render.image_settings.file_format = 'OPEN_EXR'
scn.render.image_settings.color_mode = 'RGBA'
scn.render.image_settings.color_depth = '32'
img_path = bpy.path.abspath('//')
img_file = image.name+'.exr'
image.save_render(img_path+img_file, scene=scn)
try:
    # Remove the scene (pre-2.78)
    bpy.data.scenes.remove(scn)
except:
    # Remove the scene, (Blender 2.78+)
    bpy.data.scenes.remove(scn, do_unlink=True)


bpy.data.images.remove(image)
bpy.ops.image.open(filepath=img_path+img_file)
image = bpy.data.images[img_file]
image.pack()

os.remove(img_path+img_file)

# Add a fake user to ensure the image is retained
image.use_fake_user = True

Once we have the stored 'initial' locations of the objects, we can use an Image Texture node and some simple maths to use the Object ID to calculate the location of the associated pixel to extract the color (to use as a vector) from the image as follows :

material

To convert the Object ID into a pixel location we need to add 0.5 (so that we sample the middle of a pixel) and then divide by the total number of pixels across the image. This will result in a value between 0.0 and 1.0 corresponding to the middle of the relevant pixel. Passing this into a Combine XYZ node with Y set to 0.5 produces the required vector. The Image Texture node should be set to 'Non-color Data' to prevent any color conversion (by default Blender will convert from sRGB colorspace into Linear colorspace).

To demonstrate this in action, here's an animation moving the blocks and using the 'current' location to generate the color (ie, not using the stored 'initial' location) - notice the blocks changing color as they move.

animated - actual location

And here using the captured 'initial' location extracted from the generated image using the above script and material.

animated - captured location

$\endgroup$
  • $\begingroup$ Tnx, it's complex but works very well. $\endgroup$ – Lefthandmedia Mar 30 '17 at 12:37
  • $\begingroup$ Is this technique still the most straightforward? Will the same technique work with (Blender Render) not (Cycles Render)? I currently use Pass Index in a different way. Blender users may want to know if improvements in nodes exist on this date May 13 2018. Different Node systems have different capabilities. Have Blender improvements affected this answer? $\endgroup$ – atomicbezierslinger May 13 '18 at 15:27
  • $\begingroup$ @atomicbezierslinger I’m not aware or any new functionality in the ltest release (2.79) that would allow this to be achieved in a different way. Consider raising a new question (possibly referencing this one) to see if anyone else is away of anything. $\endgroup$ – Rich Sedman May 13 '18 at 18:43
  • $\begingroup$ @RichSedman Merci $\endgroup$ – atomicbezierslinger May 14 '18 at 18:31

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.