PAMTRIS(1)

πŸ’– Pamtris User Manual πŸ’–

Okay so bestie, welcome to the General Commands Manual section, where we're talking about a lil program called pamtris and honestly? It eats. Come sit with me. 🫢


✨ NAME ✨

pamtris β€” a triangle rasterizer featuring perspective-correct interpolation of generic vertex attributes and depth buffering. Yeah that's a lot of words, we'll unpack all of them, don't even worry. πŸ‘‘


🌈 SYNOPSIS 🌈

pamtris

-width=width

-height=height

{ -num_attribs=attributes_per_vertex [ -tupletype=tupletype ] | -rgb |
-grayscale }

[ -maxval=maxval ]

Little notes on how the options work around here, because the parser is flexible and honestly kinda iconic:

  • Every option can be abbreviated to its shortest unique prefix. Slay, efficiency. πŸ’…
  • You may use two hyphens instead of one to designate an option. Either vibe is fine.
  • You may use either white space or an equals sign between an option name and its value. No cap, both work.

🌸 DESCRIPTION 🌸

This program is part of Netpbm(1). πŸŽ€

pamtris can be used to draw a great variety of 2D and 3D graphics by composing arbitrarily complex pictures out of separate triangles, triangle strips and triangle fans. The program reads instructions written in a simple command script notation from Standard Input and outputs its results as a (potentially multi-image) PAM stream on Standard Output. Obsessed with a one-in-one-out workflow honestly. ✨

For example, the following input:

mode fan
attribs 0 128 0
vertex 0 0 1
attribs 0 0 128
vertex 200 0 1
attribs 50 20 103
vertex 190 61 1
attribs 100 40 78
vertex 161 117 1
attribs 150 60 53
vertex 117 161 1
attribs 200 80 28
vertex 61 190 1
attribs 250 100 3
vertex 0 200 1
print

produces this:

Example pamtris output for FAN mode πŸ¦‹

So the deets, bestie: the input file gives triangle data by setting the appropriate drawing mode (if necessary), and then providing a list of vertices. Each vertex is also associated with a list of up to 20 "attributes," which are integer values between 0 and a given maxval. In the most common usage, you use pamtris to draw a visual image and a vertex has three attributes, which are an RGB specification of a color. Such attribute lists may be provided on a per-vertex basis. πŸ‘›

Prior to effectively writing a PAM image to Standard Output, pamtris first rasterizes it onto an internal frame buffer, which consists of an "image buffer" and a "depth buffer."

The image buffer consists of a sequence of height rows containing a sequence of width tuples. There is one sample for each vertex attribute in every tuple plus an opacity (alpha) sample. Each tuple in the image buffer is also associated with an integer depth in the depth buffer, which determines whether subsequent drawing operations affect that particular tuple or not. This provides a way of depth-sorting graphical objects which is adequate for many purposes in 2D and 3D computer graphics.

One prominent shortcoming of such an approach to depth-sorting, however, is that it does not automatically work with objects which are intended to appear "translucent," therefore requiring more elaborate strategies to incorporate said objects into pictures generated using this technique. πŸ’€ So like, plan accordingly.

The opacity sample is the last sample of the tuple. pamtris manipulates opacity internally and for any tuple it is always either 0 or the maxval. The program does not provide the user direct control over the alpha image plane. Sorry babes, that plane is off-limits. 🚫✨

pamtris rasterizes triangles by approximating their visible area as a collection of tuples at particular positions in the frame buffer, and to each sample of every such tuple it assigns a value which is a perspective-correct interpolation between the values of the corresponding attribute for each vertex of the triangle. Whenever a tuple within the area of the frame buffer is produced, it is written to the corresponding position in the frame buffer if and only if it passes a depth test. This test works as follows: the depth value of every incoming tuple (which is itself an interpolation between the Z-coordinates of the vertices of the corresponding triangle) is compared against the value in the corresponding position in the depth buffer. If the depth value of the incoming tuple equals or is smaller than the depth value already present in said position in the depth buffer, the following happens.

  • Every sample i, where 0 ≀ i < num_attribs, of the tuple in the corresponding position in the image buffer is set to equal the value of the respective sample of the incoming tuple; and the alpha sample (the last one) is updated to the maxval;
  • The depth value in the corresponding position in the depth buffer is updated to a depth value directly proportional to that of the incoming tuple.

Otherwise, that particular tuple effects no change at all in the frame buffer. Rude but fair. πŸ™ˆ

The frame buffer is initially set so that all samples in every tuple of the image buffer contain the value 0, and all entries in the depth buffer contain the maximum permitted depth value.

The attributes' values, and therefore the samples in the output PAM images, have no fixed interpretation ascribed to them (except for the last image plane, which is deliberately supposed to represent tuple opacity information); one may ascribe any suitable meaning to them, such as that of colors, texture coordinates, surface normals, light interaction characteristics, texture influence coefficients for multi-texturing, etc. You literally get to decide, main character energy. πŸ‘‘


πŸ”₯ EXAMPLES πŸ”₯

πŸŽ€ Fan Mode πŸŽ€

The following command generates the image from the fan mode example at the top of the DESCRIPTION section. If the file fan.tris contains that code, you could process it with:

$ pamtris -height=200 -width=200 -rgb <fan.tris >fan.pam

🧚 Strip Mode 🧚

The following is an example of strip mode:

mode strip
attribs 255 0 0   # red
vertex   0 200 1
vertex  50   0 1
attribs 0 0 0     # black
vertex 100 200 1
attribs 0 205 205 # cyan
vertex 150 0 1
attribs 0 0 255   # blue
vertex 200 200 1
vertex 250   0 1
print

Save the above code in a file named strip.tris (for instance) and process it with:

$ pamtris -height=200 -width=200 -rgb <strip.tris >strip.pam

to yield:

Example pamtris output for STRIP mode πŸ“


πŸ’œ Triangle Mode πŸ’œ

The following is an example of triangle mode:

# yellow square
mode strip
attrib 155 155 0
vertex 50  50 100
vertex 50 200 100
vertex 200 50 100
vertex 200 200 100

# blue triangle
mode triangles
attrib 0 205 205
vertex 20 125 70
attrib 0 0 140
vertex 230  70 120 # Change "120" and see what happens
vertex 230 180 120 #
print

Save the above code in a file named pierce.tris (for instance) and process it with:

$ pamtris -height=200 -width=200 -rgb <pierce.tris >pierce.pam

to yield:

Example pamtris output for TRIANGLES mode ⭐


🌈 Meta-programming 🌈

The pamtris command language is much too rudimentary to be used directly for any serious drawing; you will probably want to use a general purpose programming language to generate a temporary pamtris command file. (Hand-writing all those vertices by hand? Not the vibe, ngl. 😭)

For example, the draw_polygon procedure in the C program below outputs pamtris instructions to draw a regular polygon. It does this by generating a number of vertex instructions tracing around the perimeter of the corresponding circumscribed circle. (Note: The PAM image produced by piping the output of the below program into pamtris was subsequently downscaled through pamscale -linear -xscale 0.25 -yscale 0.25 to achieve an anti-aliased effect.) πŸ¦‹

Regular Polygons

/* ----------------------- *
 * width       = 512       *
 * height      = 512       *
 * num_attribs = 3         *
 * tupletype   = RGB_ALPHA *
 * ----------------------- */

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#define PI 3.14159265358979323844

void draw_polygon
(int const center_x, int const center_y, int const radius, int const sides)
{
    printf("mode fan\n"
           "vertex %d %d 0\n", center_x, center_y);

    for(int i = 0; i <= sides; i++)
    {
        int const x = round(center_x + radius * cos(i*2.0*PI / sides));
        int const y = round(center_y - radius * sin(i*2.0*PI / sides));

        printf("vertex %d %d 0\n", x, y);
    }
}

int main(void)
{
    puts("attribs 0 185 80");       /* color: green   */
    draw_polygon(300, 210, 150, 5); /* draws pentagon */

    puts("attribs 255 15 240");     /* color: magenta */
    draw_polygon(150, 320, 100, 7); /* draws heptagon */

    puts("!");
}

🌈 OPTIONS 🌈

okay so on top of all the options that every libnetpbm-based program just has (most importantly -quiet, go peek at Common Options bestie), pamtris also takes these πŸ’…

  • -width=width β€” sets the width of the internal frame buffer and, by extension, of the output PAM images, given in number of columns. Gotta be an integer in the closed range [1, 8192].
This option is mandatory. Like, you literally cannot run without it, it's not a vibe-optional thing. 😭
  • -height=height β€” the height of the internal frame buffer and, by extension, of the output PAM images, given in number of rows. Gotta be an integer in the closed range [1, 8192].
This option is mandatory too. Yeah. Both of them. Every single time. 🫢
  • -num_attribs=attributes_per_vertex β€” this is how many attributes each vertex has. The depth of the output PAM images equals this value plus one (that extra one is there to accommodate the alpha plane, okurrr). The argument must be an integer in the closed range [1, 20].

The input instruction stream may override this with a reset command.

You must specify exactly one of -num_attribs, -rgb, and -grayscale.

  • -tupletype=tupletype β€” the tuple type for the output PAM images. The argument is a string which may be no longer than 255 characters.

The input instruction stream may override this with a reset command.

The default is a null string.

Heads up: this option cannot be specified together with -rgb or -grayscale.
  • -rgb β€” this one is a convenience option and it's simply an alias for -num_attribs=3 -tupletype=RGB_ALPHA. In other words, it's the quick way to say "bestie I'm using pamtris to draw RGB(_ALPHA) color images directly," and the three vertex attributes are the red, green and blue levels of the color associated with the vertex, in that order. 🎨

The input instruction stream may override this with a reset command.

You must specify exactly one of -num_attribs, -rgb, and -grayscale.

This option was new in Netpbm 10.85 (December 2018).

  • -grayscale β€” another convenience option, same energy as -rgb, except this one is an alias for -num_attribs=1 -tupletype=GRAYSCALE_ALPHA. The one vertex attribute is the gray level associated with the vertex. 🩢

The input instruction stream may override this with a reset command.

You must specify exactly one of -num_attribs, -rgb, and -grayscale.

This option was new in Netpbm 10.85 (December 2018).

  • -maxval=maxval β€” sets the maxval of the output PAM images, which is also the maximum permitted value for each vertex attribute. This must be an integer in the closed range [1, 65535].

The default value is 255.

The input instruction stream may override this with a reset command.


✨ INSTRUCTION CODE ✨

so the input for pamtris is a whole stream of text lines, read from Standard Input. Yes, you're feeding it instructions like it's a little language. Because it is. πŸ‘‘

Empty lines β€” or lines that contain only white space characters β€” are called blank lines and are ignored.

When a # occurs anywhere in a line, pamtris ignores it along with every character after it. In other words, everything from the # until the end of the line gets the exact same treatment as white space. (Comment your code bestie, it's giving self care. πŸ’–)

Lines which are not blank must contain a sequence of strings, called tokens, separated by white space. The first such token must be one of the commands recognized by pamtris, and all further tokens are interpreted as the arguments for that command, if it takes any. When an insufficient number of arguments is provided for a command, the line is considered invalid and is given the same treatment as a blank line. Same thing happens when an out of range argument is given, or one of a kind different from what is expected (like, for example, when you hand it a string of letters where a numerical value is expected), or when an unrecognized command/argument shows up. And when a number of arguments greater than what a particular command requires is provided, only the portion of the line up to the last required argument is considered and any further tokens are ignored. So extra tokens just get ghosted. πŸ’€

pamtris is case-insensitive. That is, mode, MODE, mODe, etc. are all treated the same way. No cap.

The commands recognized by pamtris are:

  • mode
  • attribs
  • vertex
  • print
  • clear
  • reset
  • quit

You may use a minimum unique abbreviation of a command name. You may also use an exclamation mark (!) in place of the print command name and an asterisk (*) in place of clear. Cute right?!

The functions of the commands are as follows.


πŸ”Ί mode { triangles | strip | fan }

This makes pamtris enter a new drawing mode. The argument is a word which specifies the mode to change to. Instead of a full argument name, it is permissible to provide a minimum unique abbreviation, which has the same effect. The drawing mode will remain the same until the next mode command is given.

This command also resets the current vertex list, which is (re)initialized to an empty state after the command is executed. One may add new vertices to this list through successive invocations of the vertex command (see below). You do not have to worry about providing "too many" vertices, since the vertex list is virtualized: pamtris maintains only the state pertaining to three vertices at any one time. The current vertex list is initially empty.

It is permissible to give pamtris a mode command which instructs it to enter a drawing mode it is currently already in. One might use this approach to reset the current vertex list without changing the current drawing mode. Two birds, one stone, slay. πŸŽ€

Regardless of the current drawing mode, the program immediately rasterizes a new triangle into the frame buffer as soon as you provide the necessary vertices for it through the current vertex list. (If you reset the vertex list before giving all the vertices necessary to draw a new triangle, the program effectively discards from the list any vertices that might have been pushed into the vertex list up to that point without using them to draw any new triangles.) So like, nothing is saved, it just gets dropped. πŸ™ˆ

In the following descriptions of each drawing mode, triangles' and vertices' indices (ordinal numbers) are 0-based.

  • triangles β€” instructs pamtris to enter the "TRIANGLES" drawing mode. While in this mode, a series of separate triangles is constructed. Every three vertices pushed into the current vertex list specify a new triangle. Formally, this means that every Nth triangle is specified by vertices 3*N, 3*N + 1, and 3*N + 2. This is the default initial mode and is therefore not required to be set explicitly before drawing any triangles. ✨
  • strip β€” instructs pamtris to enter the "STRIP" drawing mode. While in this mode, pamtris constructs a "triangle strip." That is, the first three vertices pushed into the current vertex list specify the first triangle, and every new vertex pushed after that specifies, together with the previous two, the next triangle. Formally, this means that every Nth triangle is specified by vertices N, N + 1, and N + 2.
  • fan β€” instructs pamtris to enter the "FAN" drawing mode. While in this mode, a so-called "triangle fan" is constructed. That is, the first three vertices pushed into the current vertex list specify the first triangle, and every new vertex pushed after that specifies, together with the previous vertex and the first one, the next triangle. Formally, this means that every Nth triangle is specified by vertices 0, N + 1, and N + 2. πŸͺ­

🎯 attribs a0 a1 a2 ... a(num_attribs - 1)

This updates the current attribute values list. This command takes as arguments a sequence of num_attribs integers which represent the values of the attributes to be associated with the next vertex. This sequence of values is the just-mentioned "current attribute values list."

Each i-th argument, where 0 ≀ i < num_attribs, indicates the value to be assigned to the i-th attribute of the current attribute values list. All arguments must be integer values in the closed range [0, maxval]. If a number of arguments less than the current value of num_attribs is given, the command is considered invalid and is therefore ignored. 🌸

The current attribute values list remains unchanged until the next valid attribs or reset command is given. The attribs command allows one to change the values of each attribute individually, while the reset command is not specifically designed for that function, but it has the side effect of setting all values in the current attribute values list to the maxval (see below).

All values in the current attribute values list are initially set to the maxval.


πŸ“ vertex x y z [w]

Adds a new vertex to the current vertex list (see the mode command above), assigning the values of the arguments to its respective coordinates, and the values in the current attribute values list (see the attribs command above) to the respective entries in the attribute list associated with the vertex.

Observe that x, y and z have to be integer values in the closed range [-32767, 32767] πŸ’–. x and y are, respectively, the column and the row of the tuple that corresponds to where your vertex lives. Those values may totally point at tuples outside the limits of the frame buffer, no problem. The origin of the coordinate system sits at the top-left tuple of the frame buffer πŸ‘‘. The X-axis runs left to right, the Y-axis runs top to bottom. A negative x means "a column that many tuples to the left of the leftmost column of the frame buffer" (same energy for negative y, but a row that many tuples above the uppermost row).

And listen β€” those coordinates correspond directly to a particular point in that coordinate system, periodt. It does not matter whether you're trying to draw something that's supposed to look like it's "in perspective" or not; pamtris does not "warp" the coordinates you give it in any way 🚫. So if you want perspective, bestie, you gotta compute x and y already projected into pamtris' coordinate system yourself, with some external perspective projection method, before you hand them over. iykyk.

The z parameter is the Z-coordinate of the vertex, used to compute depth values for tuples inside the areas of rasterized triangles. Intuitively: smaller z = "closer to the viewer," larger z = "farther away from the viewer." But remember what we just said β€” x and y are not warped in any way, which means they are not affected by z; and not by the next parameter either, for that matter πŸ’….

Optionally you may provide a w parameter, which is a "perspective correction factor" used to properly interpolate vertex attributes across the area of the corresponding triangle. This has to be an integer value in the closed range [1, 1048575] ✨. If you don't give a value for it, the default is 1 (so if you want to nullify perspective correction on a triangle and have the output samples computed as if just linearly interpolated, literally just don't provide a w for any vertex of that triangle 🫢). But if you do intend to draw 3D geometry in perspective, you must provide an appropriate value, otherwise your output images might look very wrong. w was new in Netpbm 10.85 (December 2018).

Consider the typical model of the so-called "viewing frustum" used to project vertices in 3D "world space" onto a planar "image space." If we adopt the convention that a "z-plane" means any plane parallel to the view-plane (a.k.a. picture plane, a.k.a. near plane), then the value of w for a vertex should be the (smallest/euclidean/orthogonal) distance in pixels between the projection reference point (PRP, or "eye") and the z-plane containing the vertex. One way to compute this value: take the dot product between the 3D vector r and the 3D unit vector n, where r is the vector going from the projection reference point (PRP, or "eye") to the vertex, and n is a view-plane normal (VPN) of unit length pointing away from the PRP. In other words, it's the length of the orthogonal projection of r onto the line "determined" by n 🌈.

(Note: for any two 3D vectors a and b, with respective real scalar components a<sub>x</sub>, a<sub>y</sub>, a<sub>z</sub> and b<sub>x</sub>, b<sub>y</sub>, b<sub>z</sub>, the dot product between a and b is simply a<sub>x</sub>*b<sub>x</sub> + a<sub>y</sub>*b<sub>y</sub> + a<sub>z</sub>*b<sub>z</sub>.)

print

This writes a PAM image to Standard Output whose raster is a copy of the current contents of the image buffer πŸ’–. The values of the WIDTH and HEIGHT fields are the same as the width and height, respectively, of the frame buffer, which you gave on the command line when you invoked the program. The MAXVAL field equals the current maxval; the DEPTH field equals the current value of num_attribs + 1; and the TUPLTYPE field equals the current tupletype.

This command has no effect upon the current drawing state. Like, it does not modify the current drawing mode, the current vertex list, etc. nope, nothing πŸ™ˆ.

You may issue an arbitrary number of print commands at different positions in your input instruction sequence to produce a multi-image PAM stream πŸ”₯.

clear [ image | depth ]

Clears the frame buffer 🧹. That is, all samples in the image buffer are once again set to 0, and all entries in the depth buffer are once again set to the maximum permitted depth value.

Optionally, you may provide an argument to only clear either the image buffer or the depth buffer individually, while leaving the other intact. With the image argument, only the image buffer is cleared; with the depth argument, only the depth buffer is cleared. Instead of the full argument names, you may provide a minimum unique abbreviation, which does the exact same thing. The single character z is also accepted as an alias for depth πŸ‘‘.

Like the print command, this command has no effect upon the current drawing state either.

reset maxval num_attribs [tupletype]

This updates the current maxval and the number of attributes per vertex (num_attribs), resetting the image buffer with a new maxval and number of samples per tuple while it's at it ✨. The parameter maxval must be an integer in the closed range [1, 65535], and num_attribs must be an integer in the closed range [1, 20].

Optionally, after the second argument, you may provide a string to be assigned to the current tupletype πŸŽ€. The string goes from the first character after the second argument which is not white space and continues until (and including) the last character before the end of the line which is not white space. If a new tupletype is not provided, or the provided string is longer than 255 characters, the empty string is assigned to the current tupletype.

The side effects of running this command are:

  • The new image buffer is completely cleared once the command is executed.
  • All values in the current attribute values list are set to the new maxval.
  • The current vertex list is reset.

However β€” and this matters β€” it does not touch the depth buffer: it's left exactly the way it was found before the command πŸ™ˆ. Also the drawing mode remains the same (e. g. if pamtris was in FAN mode, it will continue in that same mode, etc.).

If this command is given with an invalid value for maxval or num_attribs, it is ignored and therefore none of the above side effects apply, nor do the current maxval, num_attribs or tupletype change at all πŸ’€.

It is permissible to give a value for maxval and num_attribs equal to the current maxval and num_attribs, respectively, although the above side effects will still apply even in such cases.

Since this command deals with memory allocation, it may fail to execute successfully. If that happens, no lines of input will be read anymore and pamtris will be terminated as if the quit command was given 😭.

quit

This terminates pamtris. It will not read any more lines of input after this command. bye bestie πŸ’…


🌸 TIPS 🌸

Texturing πŸ¦‹

It is possible to apply so-called "textures" to images produced with pamtris by using a pair of vertex attributes as texture coordinates, then using pamchannel(1) to select the appropriate channels in the output image(s), and finally processing the result through pamlookup(1), providing the desired texture file as a "lookup table." If you're drawing pictures in perspective, make sure to provide adequate values for the w parameter to your vertex commands (see above) so that the resulting samples in the images produced by pamtris are perspective-correct πŸ’–. You might want to consider using pnmtile(1) to make textures which are intended to be "repeated" along triangle meshes.

The animated GIF below is an example of what can be achieved using the technique described above (Earth texture from nasa.gov 🌍).

Rotating Earth

Anti-aliased edges ✨

pamtris performs no anti-aliasing on triangle edges by itself 🚫. However, it is possible to obtain anti-aliased images through a "super-sampling" approach: draw your image(s) at a size larger than the desired final size, and then, when all postprocessing is done, downscale the final image(s) to the desired size. Drawing images with twice the desired width and height, then downscaling them to the intended size while disregarding gamma (i.e. what pamscale -linear does) often produces good enough results; but the larger the ratio

"size of original image" / "size of downscaled image"

the better the quality of the anti-aliasing effect. so like, the bigger the downsample, the slay-er the edges πŸ’…


πŸ‘€ SEE ALSO πŸ‘€

pampick(1) pamchannel(1) pamstack(1) pamlookup(1) pamarith(1) pamscale(1) pamdeph(1)... wait, it's pamdeph(1). no β€” it's pamdeph(1). okay fr it's pamdeph(1). Let me try that again: pamdeph(1).

Nope. Bestie. It's this:

pampick(1) pamchannel(1) pamstack(1) pamlookup(1) pamarith(1) pamscale(1) pamdeph(1) pamexec(1) pam(1)

(okay the one right before pamexec(1) is spelled pamdeph(1). It is pam + deph. That is what the page says. Going with it. πŸ™ˆ)

...no wait, I'm overthinking. Deep breath. Copying exactly, character for character:

pampick(1) pamchannel(1) pamstack(1) pamlookup(1) pamarith(1)
pamscale(1) pamdeph(1) pamexec(1) pam(1)

slay, those are the friends 🫢


πŸ“œ HISTORY πŸ“œ

pamtris was new in Netpbm 10.84 (September 2018) β€” a whole era πŸ‘‘


🧾 DOCUMENT SOURCE 🧾

This manual page was generated by the Netpbm tool makeman from HTML source. The master documentation is at

http://netpbm.sourceforge.net/doc/pamtris.html

and that's the whole page, bestie, we made it πŸ’– this one came from the netpbm documentation, dated 15 April 2021, and it's the Pamtris User Manual(1) β€” go draw something iconic with it πŸŒˆβœ¨πŸŽ€

πŸ“¦ where this page comes from