1. NodeBox 1
    1. Homepage
    2. NodeBox 3Node-based app for generative design and data visualization
    3. NodeBox OpenGLHardware-accelerated cross-platform graphics library
    4. NodeBox 1Generate 2D visuals using Python code (Mac OS X only)
  2. Gallery
  3. Documentation
  4. Forum
  5. Blog

Texture tiles

I've been wondering how to colorize Mark Meyer's parametric surfaces from an image. Since we can get pixel colors from an image using Core Image this is accomplished fairly easy. One problem is that at some point on the surface the edges of the image will meet, resulting in a "break" in the composition. To remedy this, we need a tileable image with a width and height equal to the n parameter in the parametric surface script.

I'll start out from an image of a car engine. To make it a little more interesting, I composited it with an image of some rust. It looks a bit more grungy and tangible that way - and its colors are more harmonious. The result is my starting image file, engine.jpg.



The trick to create a tile from the image is to take a portion of the image that is smaller and then wrap around the right and bottom parts. The Core Image library is excellent to automate this process.

Here's how we start out:



texture-tile2Flip right

We create a duplicate of the layer and move it to the left, exactly so that the part that falls outside of the tile's bounding box ends up at the left.



Gradient mask

When we place two copies of the tile next to each other we want them to transition smoothly into each other. To do that, we add a linear gradient mask on the layer we shifted to the left.

The layer's uttermost left edge will then be completely opaque (and it matches the right edge of the layer below). Then it will become more and more transparent and fade into the layer below.

texture-tile4Merge and flip bottom

We can now merge the two layers together and use the new layer to flip the bottom to the top. The result is a tile that matches on all sides.

Now that you know how the principle works I might as well just add the Core Image script. It will save you some work fiddling with layer positions.

img = "engine.jpg"
coreimage = ximport("coreimage")
def tile(img, w=350, h=350):
    """ Returns a tileable canvas of given width and height.
    # Create a canvas from the image.
    # The canvas will have the same size as the image.
    canvas = coreimage.canvas(img)
    l = canvas.append(img)
    # Place the top left corner of the image
    # in the top left of the canvas.
    # We'll "wrap around" the overflow on the right and bottom.
    l.x = 0
    l.y = 0
    # Create a duplicate, shift it to the left
    # so only the overflow on the right is visible.
    wrap = l.duplicate()
    wrap.x = -w
    # Add a horizontal linear gradient mask to the wrap.
    # It should gradually disappear revealing the original image.
    m = wrap.mask.gradient()
    m.scale(1.0, int(l.width-w))
    m.x = w
    m.y = l.height
    # We now have a composition that is horizontally tileable.
    # We'll flatten our work to a single layer which we can
    # then wrap vertically.
    merged = canvas.flatten()
    canvas[0].hidden = True
    canvas[1].hidden = True
    l = canvas.append(merged)
    l.x = 0
    l.y = 0
    # Do the same for a vertical wrap.
    wrap = l.duplicate()
    wrap.y = -h
    m = wrap.mask.gradient()
    m.scale(1.0, int(l.height-h))
    m.x = 0
    m.y = h
    # Crop the canvas to the tile size.
    # We do this at the end, because before we needed 
    # the full image size to flatten.
    canvas.w = w
    canvas.h = h
    # This is our tile.
    # We can now add it to another canvas, or export it.
    tile = canvas.flatten()
    canvas = coreimage.canvas(w, h)
    return canvas
t = tile(img)
t.draw(0, t.h)
t.draw(t.w, 0)
t.draw(t.w, t.h)


We could improve our tiles even further. For example, we could add more layers to the tile with a radial gradient mask. This way the visible edges of the composition will be the tile (it matches with other tiles) but the center is something unique to the tile.

For now, we'll just stick with the basic tile and feed it into a parametric surface. Again using the Core Image library we can get the individual pixel colors from a layer in the canvas. To get the pixels from our tileable canvas:

t = tile(img)
p = t[0].pixels()

The default width and height of a tile is 350 x 350. If we render a trefoil knot with n = 350, it will match the pixel colors exactly. Then in the project() command we can simply use the following coloring scheme for each facet:

clr = p.get_pixel(i,j)


As you can see the result is blocky in some areas, something we can probably remedy by using bigger tiles and bigger n.


Created by Tom De Smedt