Python Tools - Session 3B

Height-Map Mosaic

January 23, 2020

Session Navigation

Overview

Building on Session 3A's basic mosaic, this session adds UV coordinate sampling to read pixel colors from the source image. Each cube's height is driven by the greyscale value of the sampled pixel, creating a 3D topographic effect. Vertex coloring is then applied so each tile displays the original image color directly on the geometry.

Key Concepts

  • UV coordinate sampling — using pm.colorAtPoint() to read pixel data at specific UV positions
  • Greyscale conversion — averaging RGB channels to derive a single brightness value
  • Height mapping — scaling cube height based on pixel brightness to create a relief effect
  • Vertex coloring — applying per-vertex RGB colors with pm.polyColorPerVertex()
  • UV math — incrementing U and decrementing V to walk through the texture in the correct direction

Code

This script extends the mosaic by sampling the image at each tile's UV position, computing height from greyscale, and applying the original color as vertex color.

"""
#############################################################################
filename    session_3b_code.py
author      Matt Osbond
course      CS115/CSX510
Brief Description:
    Mosaic Project
    Session 3B - Jan 23 2020
#############################################################################
"""

# Import the PyMel module, and create the alias "pm" for it to save on typing later
import pymel.core as pm

pm.newFile(force=True)

# Create the variables that we may want to change
# The path to the image - change this to a path on your local drive
image_file = r"C:\path\to\your\image.jpg"
# This is the size of each tile in the mosaic
tile_size = 10

# Set a multiplier for the height of each tile
height_scalar = 2

# Create a file node inside Maya for us to use
file_node = pm.createNode('file')

# Set the image path to be the image set above
file_node.fileTextureName.set(image_file)

# Now we calculate the number of tiles in each direction
# We do this by getting the resolution of the texture from the file node
# .. and dividing it by the tile size
# i.e. a resolution of 400, with tiles of 10 units, will create 40 tiles
width_count = int(file_node.outSizeX.get() / tile_size)
height_count = int(file_node.outSizeY.get() / tile_size)

# Print them out to make sure we get good values
print width_count, height_count

# Now we start the FOR loops
# Because we are counting along each axis as we go..
# .. we need to create a variable outside the loop to increment both the position and UV coords
x_pos = 0.0
u_coord = 0.0
for x in xrange(0, width_count):
    # here we add "tile_size" to the x_pos of the current iteration
    # += is short hand - the long form of this would be
    # x_pos = x_pos + tile_size
    x_pos += tile_size
    u_coord += 1.0/width_count  # Here, we INCREMENT the U direction, starting at 0

    # Same here for the Y axis
    y_pos = 0.0
    v_coord = 1.0
    for y in xrange(0, height_count):
        y_pos += tile_size
        v_coord -= 1.0/height_count  # Here we DECREMENT the V coord, starting at 1

        # Calculate the height
        # First, grab a value from the input multiplier and the tile size
        height = height_scalar * tile_size

        # colorAtPoint samples the texture on the file node, at the UV coords we specify
        # The "output" argument defines what data we get back
        rgb = pm.colorAtPoint(file_node, u=u_coord, v=v_coord, output='RGB')

        # Now calculate a pseudo greyscale, by getting the average of each channel
        greyscale = (rgb[0] + rgb[1] + rgb[2]) / 3.0

        # Some images look better if we "invert" the greyscale
        # This makes brighter pixels be smaller
        greyscale = 1.0 - greyscale

        # Multiply both together
        height = height * greyscale

        # Create a poly cube, setting the width, depth and height to be the size of the tile
        # REMEMBER - creating most objects in Maya returns a list
        # This contains both the Transform and the Shape
        # We only want the transform - the first element
        # We access the first element by using "[0]", the "zeroth" element in the list
        tile = pm.polyCube(width=tile_size, depth=tile_size, height=height)[0]

        # Move the transform node to our new coordinates
        pm.move(tile, [x_pos, height/2.0, y_pos])

        # Finally, set the vertex color of the tile to be the RGB of the sampled pixel
        # Note, the "colorDisplayOption" argument will tell Maya to display the vertex color
        # By default, it does not!
        pm.polyColorPerVertex(tile, rgb=rgb, colorDisplayOption=True)
← Session 3A Next: Session 4A →