// File: pngfilm.cpp // Authors: Philip Rideout (png stuff) // Mark Colbert (imaging stuff) // Matt Pharr and Greg Humphreys (pbrt itself) // Brief: pbrt plugin to write png files. // ToBeDone: Param for specifying bit depth of 8 (default) or 16 bits // Param for specifying iCCP or sRGB or neither (default) // Params for keywords (text annotations) // Documentation for the plug-in #include "pbrt.h" #include "film.h" #include "color.h" #include "paramset.h" #include "tonemap.h" #include "sampling.h" #include extern "C" DLLEXPORT Film* CreateFilm(const ParamSet& params, Filter* filter); struct Pixel { Pixel() : L(0.f) { alpha = 0.f; weightSum = 0.f; } Spectrum L; float alpha, weightSum; }; class PngFilm : public Film { public: PngFilm::PngFilm(int xres, int yres, Filter *filt, const float crop[4], const string &filename, int wf); ~PngFilm(); void AddSample(const Sample &sample, const Ray &ray, const Spectrum &L, float alpha); void GetSampleExtent(int *xstart, int *xend, int *ystart, int *yend) const; void WriteImage(); private: static const int filterTableSize = 16; Filter *filter; int writeFrequency, sampleCount; string filename; float cropWindow[4]; int xPixelStart, yPixelStart, xPixelCount, yPixelCount; string toneMapper; float bloomWidth, bloomRadius, screenGamma, fileGamma, dither; BlockedArray *pixels; float *filterTable; friend extern DLLEXPORT Film* CreateFilm(const ParamSet& params, Filter* filter); }; PngFilm::~PngFilm() { delete pixels; delete filter; delete[] filterTable; } // PngFilm Method Definitions PngFilm::PngFilm(int xres, int yres, Filter *filt, const float crop[4], const string &fn, int wf) : Film(xres, yres) { filter = filt; memcpy(cropWindow, crop, 4 * sizeof(float)); filename = fn; writeFrequency = sampleCount = wf; // Compute film image extent xPixelStart = Ceil2Int(xResolution * cropWindow[0]); xPixelCount = max(1, Ceil2Int(xResolution * cropWindow[1]) - xPixelStart); yPixelStart = Ceil2Int(yResolution * cropWindow[2]); yPixelCount = max(1, Ceil2Int(yResolution * cropWindow[3]) - yPixelStart); // Allocate film image storage pixels = new BlockedArray(xPixelCount, yPixelCount); // Precompute filter weight table filterTable = new float[filterTableSize * filterTableSize]; float *ftp = filterTable; for (int y = 0; y < filterTableSize; ++y) { float fy = ((float)y + .5f) * filter->yWidth / filterTableSize; for (int x = 0; x < filterTableSize; ++x) { float fx = ((float)x + .5f) * filter->xWidth / filterTableSize; *ftp++ = filter->Evaluate(fx, fy); } } } void PngFilm::AddSample(const Sample &sample, const Ray &ray, const Spectrum &L, float alpha) { // Compute sample's raster extent float dImageX = sample.imageX - 0.5f; float dImageY = sample.imageY - 0.5f; int x0 = Ceil2Int (dImageX - filter->xWidth); int x1 = Floor2Int(dImageX + filter->xWidth); int y0 = Ceil2Int (dImageY - filter->yWidth); int y1 = Floor2Int(dImageY + filter->yWidth); x0 = max(x0, xPixelStart); x1 = min(x1, xPixelStart + xPixelCount - 1); y0 = max(y0, yPixelStart); y1 = min(y1, yPixelStart + yPixelCount - 1); // Loop over filter support and add sample to pixel arrays // Precompute $x$ and $y$ filter table offsets int *ifx = (int*) alloca((x1 - x0 + 1) * sizeof(int)); for (int x = x0; x <= x1; ++x) { float fx = fabsf((x - dImageX) * filter->invXWidth * filterTableSize); ifx[x-x0] = min(Floor2Int(fx), filterTableSize - 1); } int *ify = (int*) alloca((y1 - y0 + 1) * sizeof(int)); for (int y = y0; y <= y1; ++y) { float fy = fabsf((y - dImageY) * filter->invYWidth * filterTableSize); ify[y-y0] = min(Floor2Int(fy), filterTableSize - 1); } for (int y = y0; y <= y1; ++y) { for (int x = x0; x <= x1; ++x) { // Evaluate filter value at $(x,y)$ pixel int offset = ify[y - y0] * filterTableSize + ifx[x - x0]; float filterWt = filterTable[offset]; // Update pixel values with filtered sample contribution Pixel &pixel = (*pixels)(x - xPixelStart, y - yPixelStart); pixel.L.AddWeighted(filterWt, L); pixel.alpha += alpha * filterWt; pixel.weightSum += filterWt; } } // Possibly write out in-progress image if (--sampleCount == 0) { WriteImage(); sampleCount = writeFrequency; } } void PngFilm::GetSampleExtent(int *xstart, int *xend, int *ystart, int *yend) const { *xstart = Floor2Int(xPixelStart + .5f - filter->xWidth); *xend = Floor2Int(xPixelStart + .5f + xPixelCount + filter->xWidth); *ystart = Floor2Int(yPixelStart + .5f - filter->yWidth); *yend = Floor2Int(yPixelStart + .5f + yPixelCount + filter->yWidth); } void pbrt_png_error(png_structp png_, png_const_charp msg) { Error("libpng error: %s\n", msg); } void PngFilm::WriteImage() { // Convert image to RGB and compute final pixel values int nPix = xPixelCount * yPixelCount; float *rgb = new float[3 * nPix], *alpha = new float[nPix]; int offset = 0; for (int y = 0; y < yPixelCount; ++y) { for (int x = 0; x < xPixelCount; ++x) { // Convert pixel spectral radiance to RGB float xyz[3]; (*pixels)(x, y).L.XYZ(xyz); const float rWeight[3] = { 3.240479f, -1.537150f, -0.498535f }; const float gWeight[3] = {-0.969256f, 1.875991f, 0.041556f }; const float bWeight[3] = { 0.055648f, -0.204043f, 1.057311f }; rgb[3*offset ] = rWeight[0]*xyz[0] + rWeight[1]*xyz[1] + rWeight[2]*xyz[2]; rgb[3*offset+1] = gWeight[0]*xyz[0] + gWeight[1]*xyz[1] + gWeight[2]*xyz[2]; rgb[3*offset+2] = bWeight[0]*xyz[0] + bWeight[1]*xyz[1] + bWeight[2]*xyz[2]; alpha[offset] = (*pixels)(x, y).alpha; // Normalize pixel with weight sum float weightSum = (*pixels)(x, y).weightSum; if (weightSum != 0.f) { float invWt = 1.f / weightSum; rgb[3*offset ] = Clamp(rgb[3*offset ] * invWt, 0.f, INFINITY); rgb[3*offset+1] = Clamp(rgb[3*offset+1] * invWt, 0.f, INFINITY); rgb[3*offset+2] = Clamp(rgb[3*offset+2] * invWt, 0.f, INFINITY); alpha[offset] = Clamp(alpha[offset] * invWt, 0.f, 1.f); } ++offset; } } // Apply the tone mapper. // Note that no gamma processing is done here. // PNG can encode gamma information, so applying gamma would infer a needless loss of information. ParamSet toneParams; ApplyImagingPipeline(rgb,xPixelCount,yPixelCount,0,bloomRadius,bloomWidth,toneMapper.c_str(),&toneParams,1,dither,255); // // PNG writing starts here! // // xPixelCount, yPixelCount....size of cropped region // xResolution, yResolution....size of entire image (usually the same as the cropped image) // xPixelStart, yPixelStart....offset of cropped region (usually 0,0) // FILE *fp = fopen(filename.c_str(), "wb"); png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, (png_error_ptr) pbrt_png_error, NULL); png_infop info = png_create_info_struct(png); png_init_io(png, fp); // Registered PNG keywords are: // Title 7 Mar 95 PNG-1.2 // Author 7 Mar 95 PNG-1.2 // Description 7 Mar 95 PNG-1.2 // Copyright 7 Mar 95 PNG-1.2 // Creation Time 7 Mar 95 PNG-1.2 // Software 7 Mar 95 PNG-1.2 // Disclaimer 7 Mar 95 PNG-1.2 // Warning 7 Mar 95 PNG-1.2 // Source 7 Mar 95 PNG-1.2 // Comment 7 Mar 95 PNG-1.2 png_text text; text.compression = PNG_TEXT_COMPRESSION_NONE; text.key = (png_charp) "Software"; text.text = (png_charp) "pbrt"; text.text_length = 4; png_set_text(png, info, &text, 1); png_color_16 black = {0}; png_set_background(png, &black, PNG_BACKGROUND_GAMMA_SCREEN, 0, 255.0); // gAMA: // // From the PNG spec: (http://www.w3.org/TR/PNG/) // // Computer graphics renderers often do not perform gamma encoding, instead // making sample values directly proportional to scene light intensity. If // the PNG encoder receives sample values that have already been // quantized into integer values, there is no point in doing gamma encoding // on them; that would just result in further loss of information. The // encoder should just write the sample values to the PNG datastream. This // does not imply that the gAMA chunk should contain a gamma value of 1.0 // because the desired end-to-end transfer function from scene intensity to // display output intensity is not necessarily linear. However, the desired // gamma value is probably not far from 1.0. It may depend on whether the // scene being rendered is a daylight scene or an indoor scene, etc. // // Given the above text, I decided that the user's requested gamma value // should be directly encoded into the PNG gAMA chunk; it is NOT a request // for pbrt to do gamma processing, since doing so would infer a needless // loss of information. The PNG authors say that the gamma value should // not necessarily be 1.0, but given the physically rigorous nature of // pbrt, I think it would be rare to use something other than 1.0. // // Note there is no option for premultiply alpha. // It was purposely omitted because the PNG spec states that PNG never uses this. // // cHRM: is: CIE x,y chromacities of R, G, B and white. // // x = X / (X + Y + Z) // y = Y / (X + Y + Z) png_set_gamma(png, screenGamma, fileGamma); float rgbWhite[3] = {1, 1, 1}; float rgbRed[3] = {1, 0, 0}; float rgbGreen[3] = {0, 1, 0}; float rgbBlue[3] = {0, 0, 1}; float xyzWhite[3]; float xyzRed[3]; float xyzGreen[3]; float xyzBlue[3]; Spectrum(rgbWhite).XYZ(xyzWhite); Spectrum(rgbRed).XYZ(xyzRed); Spectrum(rgbGreen).XYZ(xyzGreen); Spectrum(rgbBlue).XYZ(xyzBlue); float whiteX = xyzWhite[0] / (xyzWhite[0] + xyzWhite[1] + xyzWhite[2]); float whiteY = xyzWhite[1] / (xyzWhite[0] + xyzWhite[1] + xyzWhite[2]); float redX = xyzRed[0] / (xyzRed[0] + xyzRed[1] + xyzRed[2]); float redY = xyzRed[1] / (xyzRed[0] + xyzRed[1] + xyzRed[2]); float greenX = xyzGreen[0] / (xyzGreen[0] + xyzGreen[1] + xyzGreen[2]); float greenY = xyzGreen[1] / (xyzGreen[0] + xyzGreen[1] + xyzGreen[2]); float blueX = xyzBlue[0] / (xyzBlue[0] + xyzBlue[1] + xyzBlue[2]); float blueY = xyzBlue[1] / (xyzBlue[0] + xyzBlue[1] + xyzBlue[2]); png_set_cHRM(png, info, whiteX, whiteY, redX, redY, greenX, greenY, blueX, blueY); png_set_IHDR( png, info, xPixelCount, yPixelCount, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); unsigned char** rows = (png_bytep*) malloc(yPixelCount * sizeof(png_bytep)); rows[0] = (png_bytep) malloc(xPixelCount * yPixelCount * 4); for (int i = 1; i < yPixelCount; i++) rows[i] = rows[0] + i * xPixelCount * 4; for (int x = xPixelStart; x < xPixelStart + xPixelCount; ++x) { for (int y = yPixelStart; y < yPixelStart + yPixelCount; ++y) { char r = (char) rgb[(x + y * xResolution) * 3 + 0]; char g = (char) rgb[(x + y * xResolution) * 3 + 1]; char b = (char) rgb[(x + y * xResolution) * 3 + 2]; char a = (char) (255.0 * alpha[x + y * xResolution]); rows[y - yPixelStart][x * 4 + 0] = r; rows[y - yPixelStart][x * 4 + 1] = g; rows[y - yPixelStart][x * 4 + 2] = b; rows[y - yPixelStart][x * 4 + 3] = a; } } png_set_rows(png, info, rows); png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL); fclose(fp); // Release temporary image memory delete[] alpha; delete[] rgb; } extern "C" DLLEXPORT Film* CreateFilm(const ParamSet& params, Filter* filter) { string filename = params.FindOneString("filename", "pbrt.png"); int xres = params.FindOneInt("xresolution", 640); int yres = params.FindOneInt("yresolution", 480); float crop[4] = { 0, 1, 0, 1 }; int cwi; const float *cr = params.FindFloat("cropwindow", &cwi); if (cr && cwi == 4) { crop[0] = Clamp(min(cr[0], cr[1]), 0., 1.); crop[1] = Clamp(max(cr[0], cr[1]), 0., 1.); crop[2] = Clamp(min(cr[2], cr[3]), 0., 1.); crop[3] = Clamp(max(cr[2], cr[3]), 0., 1.); } int writeFrequency = params.FindOneInt("writefrequency", -1); PngFilm* film = new PngFilm(xres, yres, filter, crop, filename, writeFrequency); film->toneMapper = params.FindOneString("tonemapper", "maxwhite"); film->bloomWidth = params.FindOneFloat("bloomwidth", 0.0f); film->bloomRadius = params.FindOneFloat("bloomradius", 0.0f); film->fileGamma = params.FindOneFloat("filegamma", 1.0f); film->screenGamma = params.FindOneFloat("screengamma", 2.2f); film->dither = params.FindOneFloat("dither", 0.0f); return film; }