1 /*
2   Teem: Tools to process and visualize scientific data and images             .
3   Copyright (C) 2012, 2011, 2010, 2009  University of Chicago
4   Copyright (C) 2008, 2007, 2006, 2005  Gordon Kindlmann
5   Copyright (C) 2004, 2003, 2002, 2001, 2000, 1999, 1998  University of Utah
6 
7   This library is free software; you can redistribute it and/or
8   modify it under the terms of the GNU Lesser General Public License
9   (LGPL) as published by the Free Software Foundation; either
10   version 2.1 of the License, or (at your option) any later version.
11   The terms of redistributing and/or modifying this software also
12   include exceptions to the LGPL that facilitate static linking.
13 
14   This library is distributed in the hope that it will be useful,
15   but WITHOUT ANY WARRANTY; without even the implied warranty of
16   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17   Lesser General Public License for more details.
18 
19   You should have received a copy of the GNU Lesser General Public License
20   along with this library; if not, write to Free Software Foundation, Inc.,
21   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 */
23 
24 #include "ten.h"
25 #include "privateTen.h"
26 
27 #define INFO "Generate postscript or ray-traced renderings of 3D glyphs"
28 static const char *_tend_glyphInfoL =
29   (INFO
30    ".  Whether the output is postscript or a ray-traced image is controlled "
31    "by the initial \"rt\" flag (by default, the output is postscript). "
32    "Because this is doing viz/graphics, many parameters need to be set. "
33    "Use a response file to simplify giving the command-line options which "
34    "aren't changing between invocations. "
35    "The postscript output is an EPS file, suitable for including as a figure "
36    "in LaTeX, or viewing with ghostview, or distilling into PDF. "
37    "The ray-traced output is a 5 channel (R,G,B,A,T) float nrrd, suitable for "
38    "\"unu crop -min 0 0 0 -max 2 M M \" followed by "
39    "\"unu gamma\" and/or \"unu quantize -b 8\".");
40 
41 #define _LIMNMAGIC "LIMN0000"
42 
43 int
_tendGlyphReadCams(int imgSize[2],limnCamera ** camP,unsigned int * numCamsP,FILE * fin)44 _tendGlyphReadCams(int imgSize[2], limnCamera **camP,
45                    unsigned int *numCamsP, FILE *fin) {
46   static const char me[]="_tendGlyphReadCams";
47   char line[AIR_STRLEN_HUGE];
48   int ki;
49   double di, dn, df, fr[3], at[3], up[3], va, dwell;
50   airArray *mop, *camA;
51 
52   if (!( 0 < airOneLine(fin, line, AIR_STRLEN_HUGE)
53          && !strcmp(_LIMNMAGIC, line) )) {
54     biffAddf(TEN, "%s: couldn't read first line or it wasn't \"%s\"",
55              me, _LIMNMAGIC);
56     return 1;
57   }
58   if (!( 0 < airOneLine(fin, line, AIR_STRLEN_HUGE)
59          && 2 == (airStrtrans(airStrtrans(line, '{', ' '), '}', ' '),
60                   sscanf(line, "imgSize %d %d", imgSize+0, imgSize+1)) )) {
61     biffAddf(TEN, "%s: couldn't read second line or it wasn't "
62              "\"imgSize <sizeX> <sizeY>\"", me);
63     return 1;
64   }
65 
66   mop = airMopNew();
67   camA = airArrayNew((void **)camP, numCamsP, sizeof(limnCamera), 1);
68   airMopAdd(mop, camA, (airMopper)airArrayNix, airMopAlways);
69 
70   while ( 0 < airOneLine(fin, line, AIR_STRLEN_HUGE) ) {
71     airStrtrans(airStrtrans(line, '{', ' '), '}', ' ');
72     ki = airArrayLenIncr(camA, 1);
73     if (14 != sscanf(line, "cam.di %lg cam.at %lg %lg %lg "
74                      "cam.up %lg %lg %lg cam.dn %lg cam.df %lg cam.va %lg "
75                      "relDwell %lg cam.fr %lg %lg %lg",
76                      &di, at+0, at+1, at+2,
77                      up+0, up+1, up+2, &dn, &df, &va,
78                      &dwell, fr+0, fr+1, fr+2)) {
79       biffAddf(TEN, "%s: trouble parsing line %d: \"%s\"", me, ki, line);
80       airMopError(mop); return 1;
81     }
82     (*camP)[ki].neer = dn;
83     (*camP)[ki].faar = df;
84     (*camP)[ki].dist = di;
85     ELL_3V_COPY((*camP)[ki].from, fr);
86     ELL_3V_COPY((*camP)[ki].at, at);
87     ELL_3V_COPY((*camP)[ki].up, up);
88     (*camP)[ki].fov = va;
89     (*camP)[ki].aspect = (double)imgSize[0]/imgSize[1];
90     (*camP)[ki].atRelative = AIR_FALSE;
91     (*camP)[ki].orthographic = AIR_FALSE;
92     (*camP)[ki].rightHanded = AIR_TRUE;
93   }
94 
95   airMopOkay(mop);
96   return 0;
97 }
98 
99 int
tend_glyphMain(int argc,const char ** argv,const char * me,hestParm * hparm)100 tend_glyphMain(int argc, const char **argv, const char *me,
101                hestParm *hparm) {
102   int pret, doRT = AIR_FALSE;
103   hestOpt *hopt = NULL;
104   char *perr, *err;
105   airArray *mop;
106 
107   Nrrd *nin, *emap, *nraw, *npos, *nslc;
108   char *outS;
109   limnCamera *cam, *hackcams;
110   limnObject *glyph;
111   limnWindow *win;
112   echoObject *rect=NULL;
113   echoScene *scene;
114   echoRTParm *eparm;
115   echoGlobalState *gstate;
116   tenGlyphParm *gparm;
117   float bg[3], edgeColor[3], buvne[5], shadow, creaseAngle;
118   int ires[2], slice[2], nobg, ambocc, concave;
119   unsigned int hackci, hacknumcam;
120   size_t hackmin[3]={0,0,0}, hackmax[3]={2,0,0};
121   char *hackFN, hackoutFN[AIR_STRLEN_SMALL];
122   FILE *hackF;
123   Nrrd *hacknpng, *hacknrgb;
124   NrrdRange *hackrange;
125 
126   double v2w[9], ldir[3], edir[3], fdir[3], corn[3], len;
127 
128   /* so that command-line options can be read from file */
129   hparm->respFileEnable = AIR_TRUE;
130   hparm->elideSingleEmptyStringDefault = AIR_TRUE;
131 
132   mop = airMopNew();
133   cam = limnCameraNew();
134   airMopAdd(mop, cam, (airMopper)limnCameraNix, airMopAlways);
135   glyph = limnObjectNew(1000, AIR_TRUE);
136   airMopAdd(mop, glyph, (airMopper)limnObjectNix, airMopAlways);
137   scene = echoSceneNew();
138   airMopAdd(mop, scene, (airMopper)echoSceneNix, airMopAlways);
139   win = limnWindowNew(limnDevicePS);
140   airMopAdd(mop, win, (airMopper)limnWindowNix, airMopAlways);
141   gparm = tenGlyphParmNew();
142   airMopAdd(mop, gparm, (airMopper)tenGlyphParmNix, airMopAlways);
143   eparm = echoRTParmNew();
144   airMopAdd(mop, eparm, (airMopper)echoRTParmNix, airMopAlways);
145 
146   /* do postscript or ray-traced? */
147   hestOptAdd(&hopt, "rt", NULL, airTypeFloat, 0, 0, &doRT, NULL,
148              "generate ray-traced output.  By default (not using this "
149              "option), postscript output is generated.");
150 
151   hestOptAdd(&hopt, "v", "level", airTypeInt, 1, 1, &(gparm->verbose), "0",
152              "verbosity level");
153 
154   /* which points will rendered */
155   hestOptAdd(&hopt, "ctr", "conf thresh", airTypeFloat, 1, 1,
156              &(gparm->confThresh), "0.5",
157              "Glyphs will be drawn only for tensors with confidence "
158              "values greater than this threshold");
159   hestOptAdd(&hopt, "a", "aniso", airTypeEnum, 1, 1,
160              &(gparm->anisoType), "fa",
161              "Which anisotropy metric to use for thresholding the data "
162              "points to be drawn", NULL, tenAniso);
163   hestOptAdd(&hopt, "atr", "aniso thresh", airTypeFloat, 1, 1,
164              &(gparm->anisoThresh), "0.5",
165              "Glyphs will be drawn only for tensors with anisotropy "
166              "greater than this threshold");
167   hestOptAdd(&hopt, "p", "pos array", airTypeOther, 1, 1, &npos, "",
168              "Instead of being on a grid, tensors are at arbitrary locations, "
169              "as defined by this 3-by-N array of floats. Doing this makes "
170              "various other options moot", NULL, NULL,
171              nrrdHestNrrd);
172   hestOptAdd(&hopt, "m", "mask vol", airTypeOther, 1, 1, &(gparm->nmask), "",
173              "Scalar volume (if any) for masking region in which glyphs are "
174              "drawn, in conjunction with \"mtr\" flag. ", NULL, NULL,
175              nrrdHestNrrd);
176   hestOptAdd(&hopt, "mtr", "mask thresh", airTypeFloat, 1, 1,
177              &(gparm->maskThresh),
178              "0.5", "Glyphs will be drawn only for tensors with mask "
179              "value greater than this threshold");
180 
181   /* how glyphs will be shaped */
182   hestOptAdd(&hopt, "g", "glyph shape", airTypeEnum, 1, 1,
183              &(gparm->glyphType), "box",
184              "shape of glyph to use for display.  Possibilities "
185              "include \"box\", \"sphere\", \"cylinder\", and "
186              "\"superquad\"", NULL, tenGlyphType);
187   hestOptAdd(&hopt, "sh", "sharpness", airTypeFloat, 1, 1,
188              &(gparm->sqdSharp), "3.0",
189              "for superquadric glyphs, how much to sharp edges form as a "
190              "function of differences between eigenvalues.  Higher values "
191              "mean that edges form more easily");
192   hestOptAdd(&hopt, "gsc", "scale", airTypeFloat, 1, 1, &(gparm->glyphScale),
193              "0.01", "over-all glyph size in world-space");
194 
195   /* how glyphs will be colored */
196   hestOptAdd(&hopt, "c", "evector #", airTypeInt, 1, 1, &(gparm->colEvec), "0",
197              "which eigenvector should determine coloring. "
198              "(formally \"v\") "
199              "\"0\", \"1\", \"2\" are principal, medium, and minor");
200   hestOptAdd(&hopt, "sat", "saturation", airTypeFloat, 1, 1,
201              &(gparm->colMaxSat), "1.0",
202              "maximal saturation to use on glyph colors (use 0.0 to "
203              "create a black and white image)");
204   hestOptAdd(&hopt, "ga", "aniso", airTypeEnum, 1, 1,
205              &(gparm->colAnisoType), "fa",
206              "Which anisotropy metric to use for modulating the "
207              "saturation of the glyph color", NULL, tenAniso);
208   hestOptAdd(&hopt, "am", "aniso mod", airTypeFloat, 1, 1,
209              &(gparm->colAnisoModulate),
210              "0.0", "How much to modulate glyph color saturation by "
211              "anisotropy (as chosen by \"-ga\").  "
212              "If 1.0, then glyphs for zero anisotropy "
213              "data points will have no hue. ");
214   hestOptAdd(&hopt, "gg", "gray", airTypeFloat, 1, 1, &(gparm->colIsoGray),
215              "1.0", "desaturating glyph color due to low anisotropy "
216              "tends towards this gray level");
217   hestOptAdd(&hopt, "gam", "gamma", airTypeFloat, 1, 1, &(gparm->colGamma),
218              "0.7", "gamma to use on color components (after saturation)");
219   hestOptAdd(&hopt, "emap", "env map", airTypeOther, 1, 1, &emap, "",
220              "environment map to use for shading glyphs.  By default, "
221              "there is no shading", NULL, NULL, nrrdHestNrrd);
222   hestOptAdd(&hopt, "adsp", "phong", airTypeFloat, 4, 4, &(gparm->ADSP),
223              "0 1 0 30", "phong ambient, diffuse, specular components, "
224              "and specular power");
225   hestOptAdd(&hopt, "bg", "background", airTypeFloat, 3, 3, bg, "1 1 1",
226              "background RGB color; each component in range [0.0,1.0]");
227   hestOptAdd(&hopt, "ec", "edge rgb", airTypeFloat, 3, 3, edgeColor, "0 0 0",
228              "edge RGB color; each component in range [0.0,1.0]");
229 
230   /* parameters for showing a dataset slice */
231   hestOptAdd(&hopt, "slc", "axis pos", airTypeInt, 2, 2, slice, "-1 -1",
232              "For showing a gray-scale slice of anisotropy: the axis "
233              "and position along which to slice.  Use \"-1 -1\" to signify "
234              "that no slice should be shown");
235   hestOptAdd(&hopt, "si", "slice image", airTypeOther, 1, 1, &nslc, "",
236              "Instead of showing a slice of the anisotropy used to cull "
237              "glyphs, show something else. ", NULL, NULL,
238              nrrdHestNrrd);
239   hestOptAdd(&hopt, "off", "slice offset", airTypeFloat, 1, 1,
240              &(gparm->sliceOffset), "0.0",
241              "Offset from slice position to render slice at (so that it "
242              "doesn't occlude glyphs).");
243   hestOptAdd(&hopt, "sg", "slice gamma", airTypeFloat, 1, 1,
244              &(gparm->sliceGamma), "1.7",
245              "Gamma to apply to values on slice.");
246   hestOptAdd(&hopt, "sb", "slice bias", airTypeFloat, 1, 1,
247              &(gparm->sliceBias), "0.05",
248              "amount by which to bump up slice gray values prior to gamma.");
249 
250   /* camera */
251   hestOptAdd(&hopt, "fr", "from point", airTypeDouble, 3, 3, cam->from, NULL,
252              "position of camera, used to determine view vector");
253   hestOptAdd(&hopt, "at", "at point", airTypeDouble, 3, 3, cam->at, "0 0 0",
254              "camera look-at point, used to determine view vector");
255   hestOptAdd(&hopt, "up", "up vector", airTypeDouble, 3, 3, cam->up, "0 0 1",
256              "camera pseudo-up vector, used to determine view coordinates");
257   hestOptAdd(&hopt, "rh", NULL, airTypeInt, 0, 0, &(cam->rightHanded), NULL,
258              "use a right-handed UVN frame (V points down)");
259   hestOptAdd(&hopt, "dn", "near clip", airTypeDouble, 1, 1, &(cam->neer), "-2",
260              "position of near clipping plane, relative to look-at point");
261   hestOptAdd(&hopt, "df", "far clip", airTypeDouble, 1, 1, &(cam->faar), "2",
262              "position of far clipping plane, relative to look-at point");
263   hestOptAdd(&hopt, "or", NULL, airTypeInt, 0, 0, &(cam->orthographic), NULL,
264              "use orthogonal projection");
265   hestOptAdd(&hopt, "ur", "uMin uMax", airTypeDouble, 2, 2, cam->uRange,
266              "-1 1", "range in U direction of image plane");
267   hestOptAdd(&hopt, "vr", "vMin vMax", airTypeDouble, 2, 2, cam->vRange,
268              "-1 1", "range in V direction of image plane");
269   hestOptAdd(&hopt, "fv", "fov", airTypeDouble, 1, 1, &(cam->fov), "nan",
270              "if not NaN, vertical field-of-view, in degrees");
271 
272   /* postscript-specific options */
273   hestOptAdd(&hopt, "gr", "glyph res", airTypeInt, 1, 1, &(gparm->facetRes),
274              "10", "(* postscript only *) "
275              "resolution of polygonalization of glyphs (all glyphs "
276              "other than the default box)");
277   hestOptAdd(&hopt, "wd", "3 widths", airTypeFloat, 3, 3, gparm->edgeWidth,
278              "0.8 0.4 0.0",  "(* postscript only *) "
279              "width of edges drawn for three kinds of glyph "
280              "edges: silohuette, crease, non-crease");
281   hestOptAdd(&hopt, "psc", "scale", airTypeFloat, 1, 1, &(win->scale), "300",
282              "(* postscript only *) "
283              "scaling from screen space units to postscript units "
284              "(in points)");
285   hestOptAdd(&hopt, "ca", "angle", airTypeFloat, 1, 1, &creaseAngle, "70",
286              "(* postscript only *) "
287              "minimum crease angle");
288   hestOptAdd(&hopt, "nobg", NULL, airTypeInt, 0, 0, &nobg, NULL,
289              "(* postscript only *) "
290              "don't initially fill with background color");
291   hestOptAdd(&hopt, "concave", NULL, airTypeInt, 0, 0, &concave, NULL,
292              "use slightly buggy rendering method suitable for "
293              "concave or self-occluding objects");
294 
295   /* ray-traced-specific options */
296   hestOptAdd(&hopt, "is", "nx ny", airTypeInt, 2, 2, ires, "256 256",
297              "(* ray-traced only *) "
298              "image size (resolution) to render");
299   hestOptAdd(&hopt, "ns", "# samp", airTypeInt, 1, 1, &(eparm->numSamples),"4",
300              "(* ray-traced only *) "
301              "number of samples per pixel (must be a square number)");
302   if (airThreadCapable) {
303     hestOptAdd(&hopt, "nt", "# threads", airTypeInt, 1, 1,
304                &(eparm->numThreads), "1",
305                "(* ray-traced only *) "
306                "number of threads to be used for rendering");
307   }
308   hestOptAdd(&hopt, "al", "B U V N E", airTypeFloat, 5, 5, buvne,
309              "0 -1 -1 -4 0.7",
310              "(* ray-traced only *) "
311              "brightness (B), view-space location (U V N), "
312              "and length of edge (E) "
313              "of a square area light source, for getting soft shadows. "
314              "Requires lots more samples \"-ns\" to converge.  Use "
315              "brightness 0 (the default) to turn this off, and use "
316              "environment map-based shading (\"-emap\") instead. ");
317   hestOptAdd(&hopt, "ao", NULL, airTypeInt, 0, 0, &ambocc, NULL,
318              "set up 6 area lights in a box to approximate "
319              "ambient occlusion");
320   hestOptAdd(&hopt, "shadow", "s", airTypeFloat, 1, 1, &shadow, "1.0",
321              "the extent to which shadowing occurs");
322   hestOptAdd(&hopt, "hack", "hack", airTypeString, 1, 1, &hackFN, "",
323              "don't mind me");
324 
325   /* input/output */
326   hestOptAdd(&hopt, "i", "nin", airTypeOther, 1, 1, &nin, "-",
327              "input diffusion tensor volume", NULL, NULL, nrrdHestNrrd);
328   hestOptAdd(&hopt, "o", "nout", airTypeString, 1, 1, &outS, "-",
329              "output file");
330 
331   airMopAdd(mop, hopt, (airMopper)hestOptFree, airMopAlways);
332   USAGE(_tend_glyphInfoL);
333   PARSE();
334   airMopAdd(mop, hopt, (airMopper)hestParseFree, airMopAlways);
335 
336   /* set up slicing stuff */
337   if (!( -1 == slice[0] && -1 == slice[1] )) {
338     gparm->doSlice = AIR_TRUE;
339     gparm->sliceAxis = slice[0];
340     gparm->slicePos = slice[1];
341     gparm->sliceAnisoType = gparm->anisoType;
342     /* gparm->sliceOffset set by hest */
343   }
344 
345   if (npos) {
346     fprintf(stderr, "!%s: have npos --> turning off onlyPositive \n", me);
347     gparm->onlyPositive = AIR_FALSE;
348   }
349 
350   if (gparm->verbose) {
351     fprintf(stderr, "%s: verbose = %d\n", me, gparm->verbose);
352   }
353   if (tenGlyphGen(doRT ? NULL : glyph,
354                   doRT ? scene : NULL,
355                   gparm,
356                   nin, npos, nslc)) {
357     airMopAdd(mop, err = biffGetDone(TEN), airFree, airMopAlways);
358     fprintf(stderr, "%s: trouble generating glyphs:\n%s\n", me, err);
359     airMopError(mop); return 1;
360   }
361   if (AIR_EXISTS(cam->fov)) {
362     if (limnCameraAspectSet(cam, ires[0], ires[1], nrrdCenterCell)) {
363       airMopAdd(mop, err = biffGetDone(LIMN), airFree, airMopAlways);
364       fprintf(stderr, "%s: trouble with camera:\n%s\n", me, err);
365       airMopError(mop); return 1;
366     }
367   }
368   cam->dist = 0;
369   cam->atRelative = AIR_TRUE;
370   if (limnCameraUpdate(cam)) {
371     airMopAdd(mop, err = biffGetDone(LIMN), airFree, airMopAlways);
372     fprintf(stderr, "%s: trouble with camera:\n%s\n", me, err);
373     airMopError(mop); return 1;
374   }
375   if (doRT) {
376     nraw = nrrdNew();
377     airMopAdd(mop, nraw, (airMopper)nrrdNuke, airMopAlways);
378     gstate = echoGlobalStateNew();
379     airMopAdd(mop, gstate, (airMopper)echoGlobalStateNix, airMopAlways);
380     eparm->shadow = shadow;
381     if (buvne[0] > 0) {
382       ELL_34M_EXTRACT(v2w, cam->V2W);
383       ELL_3MV_MUL(ldir, v2w, buvne+1);
384       ell_3v_perp_d(edir, ldir);
385       ELL_3V_NORM(edir, edir, len);
386       ELL_3V_CROSS(fdir, ldir, edir);
387       ELL_3V_NORM(fdir, fdir, len);
388       ELL_3V_SCALE(edir, buvne[4]/2, edir);
389       ELL_3V_SCALE(fdir, buvne[4]/2, fdir);
390       ELL_3V_ADD4(corn, cam->at, ldir, edir, fdir);
391       rect = echoObjectNew(scene, echoTypeRectangle);
392       echoRectangleSet(rect,
393                        corn[0], corn[1], corn[2],
394                        -edir[0]*2, -edir[1]*2, -edir[2]*2,
395                        -fdir[0]*2, -fdir[1]*2, -fdir[2]*2);
396       echoColorSet(rect, 1, 1, 1, 1);
397       echoMatterLightSet(scene, rect, buvne[0], 0);
398       echoObjectAdd(scene, rect);
399     }
400     if (ambocc) {
401       double eye[3], llen;
402       ELL_3V_SUB(eye, cam->from, cam->at);
403       llen = 4*ELL_3V_LEN(eye);
404 
405       ELL_3V_COPY(corn, cam->at);
406       corn[0] -= llen/2;
407       corn[1] -= llen/2;
408       corn[2] -= llen/2;
409       rect = echoObjectNew(scene, echoTypeRectangle);
410       echoRectangleSet(rect, corn[0], corn[1], corn[2],
411                        llen, 0, 0,       0, llen, 0);
412       echoColorSet(rect, 1, 1, 1, 1);
413       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
414       echoObjectAdd(scene, rect);
415       rect = echoObjectNew(scene, echoTypeRectangle);
416       echoRectangleSet(rect, corn[0], corn[1], corn[2],
417                        0, 0, llen,       llen, 0, 0);
418       echoColorSet(rect, 1, 1, 1, 1);
419       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
420       echoObjectAdd(scene, rect);
421       rect = echoObjectNew(scene, echoTypeRectangle);
422       echoRectangleSet(rect, corn[0], corn[1], corn[2],
423                        0, llen, 0,      0, 0, llen);
424       echoColorSet(rect, 1, 1, 1, 1);
425       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
426       echoObjectAdd(scene, rect);
427 
428       corn[0] += llen/2;
429       corn[1] += llen/2;
430       corn[2] += llen/2;
431       rect = echoObjectNew(scene, echoTypeRectangle);
432       echoRectangleSet(rect, corn[0], corn[1], corn[2],
433                        0, -llen, 0,         -llen, 0, 0);
434       echoColorSet(rect, 1, 1, 1, 1);
435       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
436       echoObjectAdd(scene, rect);
437       rect = echoObjectNew(scene, echoTypeRectangle);
438       echoRectangleSet(rect, corn[0], corn[1], corn[2],
439                        -llen, 0, 0,        0, 0, -llen);
440       echoColorSet(rect, 1, 1, 1, 1);
441       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
442       echoObjectAdd(scene, rect);
443       rect = echoObjectNew(scene, echoTypeRectangle);
444       echoRectangleSet(rect, corn[0], corn[1], corn[2],
445                        0, 0, -llen,      0, -llen, 0);
446       echoColorSet(rect, 1, 1, 1, 1);
447       echoMatterLightSet(scene, rect, 1, AIR_CAST(echoCol_t, llen));
448       echoObjectAdd(scene, rect);
449 
450     }
451     eparm->imgResU = ires[0];
452     eparm->imgResV = ires[1];
453     eparm->jitterType = (eparm->numSamples > 1
454                          ? echoJitterJitter
455                          : echoJitterNone);
456     eparm->aperture = 0;
457     eparm->renderBoxes = AIR_FALSE;
458     eparm->seedRand = AIR_FALSE;
459     eparm->renderLights = AIR_FALSE;
460     ELL_3V_COPY(scene->bkgr, bg);
461     scene->envmap = emap;
462     if (!airStrlen(hackFN)) {
463       /* normal operation: one ray-tracing for one invocation */
464       if (echoRTRender(nraw, cam, scene, eparm, gstate)) {
465         airMopAdd(mop, err = biffGetDone(ECHO), airFree, airMopAlways);
466         fprintf(stderr, "%s: trouble ray-tracing %s\n", me, err);
467         airMopError(mop);
468         return 1;
469       }
470       if (nrrdSave(outS, nraw, NULL)) {
471         airMopAdd(mop, err = biffGetDone(NRRD), airFree, airMopAlways);
472         fprintf(stderr, "%s: trouble saving ray-tracing output %s\n", me, err);
473         airMopError(mop);
474         return 1;
475       }
476     } else {
477       /* hack: multiple renderings per invocation */
478       if (!(hackF = airFopen(hackFN, stdin, "rb"))) {
479         fprintf(stderr, "%s: couldn't fopen(\"%s\",\"rb\"): %s\n",
480                 me, hackFN, strerror(errno));
481         airMopError(mop); return 1;
482       }
483       if (_tendGlyphReadCams(ires, &hackcams, &hacknumcam, hackF)) {
484         airMopAdd(mop, err = biffGetDone(TEN), airFree, airMopAlways);
485         fprintf(stderr, "%s: trouble reading frames %s\n", me, err);
486         airMopError(mop);
487         return 1;
488       }
489       eparm->imgResU = ires[0];
490       eparm->imgResV = ires[1];
491       hackmax[1] = ires[0]-1;
492       hackmax[2] = ires[1]-1;
493       hacknrgb = nrrdNew();
494       hacknpng = nrrdNew();
495       airMopAdd(mop, hacknrgb, (airMopper)nrrdNuke, airMopAlways);
496       airMopAdd(mop, hacknpng, (airMopper)nrrdNuke, airMopAlways);
497       hackrange = nrrdRangeNew(0.0, 1.0);
498       airMopAdd(mop, hackrange, (airMopper)nrrdRangeNix, airMopAlways);
499       for (hackci=0; hackci<hacknumcam; hackci++) {
500         memcpy(cam, hackcams + hackci, sizeof(limnCamera));
501         /* rightHanded and orthographic not handled nicely */
502 
503         if (rect) {
504           if (limnCameraUpdate(cam)) {
505             airMopAdd(mop, err = biffGetDone(LIMN), airFree, airMopAlways);
506             fprintf(stderr, "%s: trouble with camera:\n%s\n", me, err);
507             airMopError(mop); return 1;
508           }
509           ELL_34M_EXTRACT(v2w, cam->V2W);
510           ELL_3MV_MUL(ldir, v2w, buvne+1);
511           ell_3v_perp_d(edir, ldir);
512           ELL_3V_NORM(edir, edir, len);
513           ELL_3V_CROSS(fdir, ldir, edir);
514           ELL_3V_NORM(fdir, fdir, len);
515           ELL_3V_SCALE(edir, buvne[4]/2, edir);
516           ELL_3V_SCALE(fdir, buvne[4]/2, fdir);
517           ELL_3V_ADD4(corn, cam->at, ldir, edir, fdir);
518           echoRectangleSet(rect,
519                            corn[0], corn[1], corn[2],
520                            edir[0]*2, edir[1]*2, edir[2]*2,
521                            fdir[0]*2, fdir[1]*2, fdir[2]*2);
522         }
523 
524         if (echoRTRender(nraw, cam, scene, eparm, gstate)) {
525           airMopAdd(mop, err = biffGetDone(ECHO), airFree, airMopAlways);
526           fprintf(stderr, "%s: trouble ray-tracing %s\n", me, err);
527           airMopError(mop);
528           return 1;
529         }
530         sprintf(hackoutFN, "%04d.png", hackci);
531         if (nrrdCrop(hacknrgb, nraw, hackmin, hackmax)
532             || nrrdQuantize(hacknpng, hacknrgb, hackrange, 8)
533             || nrrdSave(hackoutFN, hacknpng, NULL)) {
534           airMopAdd(mop, err = biffGetDone(NRRD), airFree, airMopAlways);
535           fprintf(stderr, "%s: trouble saving output %s\n", me, err);
536           airMopError(mop);
537           return 1;
538         }
539       }
540     }
541   } else {
542     if (!(win->file = airFopen(outS, stdout, "wb"))) {
543       fprintf(stderr, "%s: couldn't fopen(\"%s\",\"wb\"): %s\n",
544               me, outS, strerror(errno));
545       airMopError(mop); return 1;
546     }
547     airMopAdd(mop, win->file, (airMopper)airFclose, airMopAlways);
548     cam->neer = -0.000000001;
549     cam->faar = 0.0000000001;
550     win->ps.lineWidth[limnEdgeTypeBackFacet] = 0;
551     win->ps.lineWidth[limnEdgeTypeBackCrease] = 0;
552     win->ps.lineWidth[limnEdgeTypeContour] = gparm->edgeWidth[0];
553     win->ps.lineWidth[limnEdgeTypeFrontCrease] = gparm->edgeWidth[1];
554     win->ps.lineWidth[limnEdgeTypeFrontFacet] = gparm->edgeWidth[2];
555     win->ps.lineWidth[limnEdgeTypeBorder] = 0;
556       /* win->ps.lineWidth[limnEdgeTypeFrontCrease]; */
557     win->ps.creaseAngle = creaseAngle;
558     win->ps.noBackground = nobg;
559     ELL_3V_COPY(win->ps.bg, bg);
560     ELL_3V_COPY(win->ps.edgeColor, edgeColor);
561     if (limnObjectRender(glyph, cam, win)
562         || (concave
563             ? limnObjectPSDrawConcave(glyph, cam, emap, win)
564             : limnObjectPSDraw(glyph, cam, emap, win))) {
565       airMopAdd(mop, err = biffGetDone(LIMN), airFree, airMopAlways);
566       fprintf(stderr, "%s: trouble drawing glyphs:\n%s\n", me, err);
567       airMopError(mop); return 1;
568     }
569   }
570 
571   airMopOkay(mop);
572   return 0;
573 }
574 /* TEND_CMD(glyph, INFO); */
575 unrrduCmd tend_glyphCmd = { "glyph", INFO, tend_glyphMain };
576