While this blog has featured mostly *old skool* vector graphics, which were very popular some 30 years back as there was basically no hardware support for texture mapping and CPU power was what it was, I decided to try that using the usual suspects Python, NumPy and pygame. Of course, handling graphics pixel by pixel is much harder (and slower) than drawing one-color surfaces. Even so, the first real time texture mapping I am aware of (in computer graphics that is) dates as far as September 1991: the Cube-o-Matic Amiga demo by Spreadpoint. Quite a feat, I must say, even if the resolution is only about 100 x 100 pixels and updated perhaps 10-15 frames per second.

My basic PC handles about 5 million pixels per second in this implementation, perhaps 50 times more than Cube-o-Matic - a very modest improvement considering the resources. I used images from the Sound Vision demo I pythonized some time ago: credits for some of the images go to Red Baron and Overlander, and Jellybean for the music.

### Reading a Map, Affinely

### How to do Texture Mapping

*tutorial mode*of the program (if

*self.tutorial_mode*is set to false, it will run like the video above). First of all, for each surface (face of the cube) I have defined the picture to be used, and also, for each node (corner) of the surface, the respective coordinate within the picture.

**rotated**surface from the top; starting point is the top of the blue triangle. Then, I can go down as far as the first corner on the left. Note that the triangle is strictly horizontal at the bottom, representing a line of pixels. To be sure to process each pixel, it is necessary to go down the blue triangle horizontal line by line, in effect increasing the y coordinate one by one, and processing all the x coordinates between the two triangle sides.

# define a cube. self.nodes = (100.0 * np.array([ [ 1, 1, 1], [ 1, 1, -1], [-1, 1, -1], [-1, 1, 1], [ 1, -1, 1], [ 1, -1, -1], [-1, -1, -1], [-1, -1, 1] ])).astype(np.float) self.surfaces = np.array([ [0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [2, 6, 7, 3], [1, 5, 6, 2], [3, 7, 4, 0] ])

**5**, 6,

**7**] is that by defining surface nodes clockwise enables later calculating if they are "facing" towards the viewer (and should be drawn) or not.)

def map_texture(self, surface): # build a node array where the nodes appear twice - enabling going "over" the right edge nodes_x2 = np.hstack((self.surfaces[surface, :], self.surfaces[surface, :])) # find the topmost node (minimum y cooridnate) and maximum y coordinate min_y_node = np.argmin(self.trans_nodes[self.surfaces[surface, :], 1]) max_y = np.max(self.trans_nodes[self.surfaces[surface, :], 1]) y_beg = self.trans_nodes[nodes_x2[min_y_node], 1] # when going "left" and "right" through the nodes, start with the top node in both (left, right) = (min_y_node, min_y_node)

# loop through each section from this y coordinate to the next y coordinate until all sections processed while y_beg < max_y: # image node depends on the node order img_node_beg = image_nodes[np.array([left % image_node_cnt, right % image_node_cnt]), :] img_node_end = image_nodes[np.array([(left - 1) % image_node_cnt, (right + 1) % image_node_cnt]), :] img_node_diff = img_node_end - img_node_beg # cube node comes from surface node list node_beg = self.trans_nodes[nodes_x2[np.array([left, right])], :] node_end = self.trans_nodes[nodes_x2[np.array([left - 1, right + 1])], :] node_diff = node_end - node_beg

**0**, respectively - going left and right from the top node 3 in[0, 1, 2, 3, 0, 1, 2, 3].

**0**. The img_node_diff and node_diff simply represent the vectors on the left and right side for both.

# find section end = y_end if node_end[1, 1] < node_end[0, 1]: # right node comes first (i.e. its Y coordinate is before left's) right += 1 y_end = node_end[1, 1] else: # left node comes first (i.e. its Y coordinate is before or equal to right's) left -= 1 y_end = node_end[0, 1]

### Section, Part, Portion, Piece, Segment, Fragment

if y_end > y_beg: y = np.arange(y_beg, y_end, dtype=np.int16) # node multipliers for each y for left and right side. Since y_end is the first node down, node_diff[:, 1] is here always > 0 m = (y[:, None] - node_beg[:, 1]) / node_diff[:, 1] # respective screen x coordinates for left and right side x = (np.round(node_beg[:, 0] + m * node_diff[:, 0])).astype(np.int16) x_cnt = np.abs(x[:, 1] - x[:, 0]) # + 1 - use +1 when using linspace method below # count cumulative pixel count to use as the offset when storing data x_cnt_cum = np.hstack((np.array([0]), np.cumsum(x_cnt))) + self.scr_cnt # respective image coordinates, interpolating between image nodes (usually corners) img_l = img_node_beg[0, :] + m[:, 0:1] * img_node_diff[0, :] img_r = img_node_beg[1, :] + m[:, 1:2] * img_node_diff[1, :]

**100**and y_end =

**120**then y would have the 20 integers starting at

**100**and ending at

**119**. The multipliers m are then calculated for each y and for both left and right side. In the blue triangle, the left multiplier would start from zero and end at

**0.95**(19 / 20), but the right multiplier would (while starting from zero as well) end at somewhere around 0.4, as that is where point m lies on the way from node 3 to node 0.

### Going Pixels

for y_line in range(y_end - y_beg): # process each horizontal line, these are the x coordinates from x left to x right if x_cnt[y_line] > 1: # if "left" not on the left side, use negative step. scr_x = np.arange(x[y_line, 0], x[y_line, 1], np.sign(x[y_line, 1] - x[y_line, 0]), dtype=np.int16) # add x coordinates to self.scr_x array self.scr_x[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1]] = scr_x # add y coordinates similarly - y is constant self.scr_y[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1]] = y_line + y_beg # interpolate between line begin and end coordinates in image self.img_xy[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1], :] = (img_l[y_line, :] + ((scr_x - scr_x[0]) / (scr_x[-1] - scr_x[0]))[:, None] * (img_r[y_line, :] - img_l[y_line, :])).astype(np.int16) # store the color found in each interpolated pixel in self.scr_col self.scr_col[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1]] = image_array[self.img_xy[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1], 0], self.img_xy[x_cnt_cum[y_line]:x_cnt_cum[y_line + 1], 1]]

**0**as the left side has moved forward while the right side has not, and y will be between the y coordinates of nodes 2 and 0.

# update screen in one go by setting the colors for the mapped pixels. if self.scr_cnt > 0: screen_array = pygame.surfarray.pixels2d(self.screen) screen_array[self.scr_x[0:self.scr_cnt], self.scr_y[0:self.scr_cnt]] = self.scr_col[0:self.scr_cnt]