// scattertable.cpp
//
// Copyright (C) 2010, Chris Laurel <claurel@shatters.net>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// A utility for generating atmospheric transmittance and scattering
// tables for use in real time 3D rendering.
//
// Using a 3D texture to store precomputed inscattering values was described
// in:
// Schafhitzel T., Falk M., Ertl T.: "Real-time rendering of planets with
// atmospheres." In WSCG International Conference in Central Europe on
// Computer Graphics, Visualization and Computer Vision (2007).
//
// The approach in Schafhitzel et al was extended in Bruneton E., Neyret F.:
// "Precomputed Atmospheric Scattering." Eurographics Symposium on
// Rendering 2008. Bruneton and Neyret made three key improvements:
// * Extended the tables to 4D in order to incorporate the angle between
//   viewer and the sun (resulting in the 'twilight wedge' phenomenon)
// * Added extra steps in the table generation to simulate the effects
//   of multiple scattering
// * Optimized the parameterizations of height, view angle, and sun angle
//   in order to reduce artifacts and use less storage. Further improvements
//   to the parameterization were made in GLSL and code available on the
//   web at http://evasion.inrialpes.fr/~Eric.Bruneton/
//
// The inscatter table produced by the scattertable utility currently only
// incorporates single scattering and is 3D, not 4D. The parameterization of
// view angle is an optimized version of the 'steep sigmoid' function
// used by Bruneton (which involved an expensive inverse trig operation
// in the shader.)

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <map>
#include <cassert>
#include <Eigen/Core>
#include <celmath/mathlib.h>

using namespace Eigen;
using namespace std;
namespace math = celestia::math;

constexpr const unsigned int HeightSamples = 32;
constexpr const unsigned int ViewAngleSamples = 256;
constexpr const unsigned int SunAngleSamples = 32;

// Values settable via the command line
static unsigned int ScatteringIntegrationSteps = 25;

typedef map<string, double> ParameterSet;

static const Vector3f RGBWavelengths(680.0f, 550.0f, 440.0f);

/** @param lambda - wavelength in nm
 *  @param n - index of refraction
 *  @param N - particles per meter^3
 */
static float RayleighScatteringCoeff(float lambda, float n, float N)
{
    float lambdaM = lambda * 1.0e-9f;
    return (8.0f * pow(float(M_PI), 3.0f) * pow(n * n - 1.0f, 2.0f)) / (3.0f * N * pow(lambdaM, 4.0f));
}

static Vector3f RayleighScatteringCoeff(float n, float N)
{
    return Vector3f(RayleighScatteringCoeff(RGBWavelengths.x(), n, N),
                    RayleighScatteringCoeff(RGBWavelengths.y(), n, N),
                    RayleighScatteringCoeff(RGBWavelengths.z(), n, N));
}


class Atmosphere
{
public:
    float shellRadius() const
    {
        return planetRadius + max(mieScaleHeight, rayleighScaleHeight) * 8.0f;
    }

    Vector3f* computeTransmittanceTable() const;
    Vector4f* computeInscatterTable() const;

    float planetRadius;
    float rayleighScaleHeight;
    float mieScaleHeight;
    Vector3f rayleighCoeff;
    float mieCoeff;
    Vector3f absorptionCoeff;
    float mieAsymmetry;
};


// Based on approximation from E. Bruneton and F. Neyret,
//     - r is distance of eye from planet center
//     - mu is the cosine of the view angle (view angle dot zenith direction)
//     - pathLength is the distance that the ray travels through the atmosphere
//     - H is the scale height
//     - R is the planet radius
float opticalDepth(float r, float mu, float l, float H, float R)
{
    float a = sqrt(r * 0.5 / H);
    float bx = a * mu;
    float by = a * (mu + l / r);
    float sbx = math::sign(bx);
    float sby = math::sign(by);
    float x = sby > sbx ? exp(bx * bx) : 0.0f;
    float yx = sbx / (2.3193f * abs(bx) + sqrt(1.52f * bx * bx + 4.0f));
    float yy = sby / (2.3193f * abs(by) + sqrt(1.52f * by * by + 4.0f)) *
        exp(-l / H * (l / (2.0f * r) + mu));

    return sqrt(6.2831f * H * r) * exp((R - r) / H) * (x + yx - yy);
}


Vector3f transmittance(float r, float mu, float l, const Atmosphere& atm)
{
    float depthR = opticalDepth(r, mu, l, atm.rayleighScaleHeight, atm.planetRadius);
    float depthM = opticalDepth(r, mu, l, atm.mieScaleHeight, atm.planetRadius);
    return (-depthR * atm.rayleighCoeff
            - depthM * Vector3f::Constant(atm.mieCoeff)
            - depthM * atm.absorptionCoeff).array().exp();
}


// Parameters:
//   h   - height of the viewpoint above the planet surface
//   mu  - cosine of the angle between the view direction and zenith
//   muS - cosine of the angle between the sun direction and zenith

// Map a unorm to the cosine of the view angle
static inline float toMu(float u)
{
    float x = u * 2.0f - 1.0f;
    float signX = x < 0.0f ? 1.0f : -1.0f;
    return (x * (0.1f - 0.15f * signX) - 0.165f) / (signX * x + 1.1f);
}

// Map a unorm to the cosine of the sun angle
static inline float toMuS(float u)
{
    // Original version from Bruneton paper:
    //return (-1.0f / 3.0f) * (log(1.0f - u * (1.0f - exp(-3.6f))) + 0.6f);

    // Modifier version has a wider range, allowing more negative angles. This
    // eliminates the faint but persistent illumination that appears even
    // when the sun is far below the horizon. Even the adjusted function may
    // still not be adequate when very large scale heights are used.
    return (-1.0f / 2.0f) * (log(1.0f - u * (1.0f - exp(-2.6f))) + 0.6f);
}


Vector3f*
Atmosphere::computeTransmittanceTable() const
{
    float Rg = planetRadius;
    float Rg2 = Rg * Rg;
    float Rt = shellRadius();
    float Rt2 = Rt * Rt;

    unsigned int sampleCount = HeightSamples * ViewAngleSamples;
    Vector3f* transmittanceTable = new Vector3f[sampleCount];

    // Avoid numerical precision problems by choosing a first viewer
    // position just *above* the planet surface.
    float baseHeight = Rg * 1.0e-6f;

    for (unsigned int i = 0; i < HeightSamples; ++i)
    {
        float v = float(i) / float(HeightSamples);
        float h = v * v * (Rt - Rg) + baseHeight;
        float r = Rg + h;
        float r2 = r * r;

        for (unsigned int j = 0; j < ViewAngleSamples; ++j)
        {
            float u = float(j) / float(ViewAngleSamples - 1);
            float mu = max(-1.0f, min(1.0f, toMu(u)));
            float cosTheta = mu;
            float sinTheta2 = 1.0f - cosTheta * cosTheta;

            float pathLength;
            float d = Rg2 - r2 * sinTheta2;
            if (d > 0.0f && -r * cosTheta - sqrt(d) > 0.0f)
            {
                pathLength = -r * cosTheta - sqrt(Rg2 - r2 * sinTheta2);
            }
            else
            {
                pathLength = -r * cosTheta + sqrt(Rt2 - r2 * sinTheta2);
            }

            unsigned int index = i * ViewAngleSamples + j;
            transmittanceTable[index] = transmittance(r, mu, pathLength, *this);

            // Warning messages
            if (isnan(transmittanceTable[index].x()))
            {
                cout << "NaN in transmittance table at (" << j << ", " << i << ")\n";
                cout << transmittanceTable[index].x() << endl;
                cout << "r=" << r << ", mu=" << mu << ", l=" << pathLength << endl;
                exit(1);
            }

            if (transmittanceTable[index].x() > 1.0f)
            {
                cout << "Non-physical transmittance " << transmittanceTable[index].x() << endl;
            }
        }
    }

    return transmittanceTable;
}


Vector4f*
Atmosphere::computeInscatterTable() const
{
    // Rg - "ground radius"
    // Rt - "transparent radius", i.e. radius of the atmosphere at some point
    //      where it is visually undetectable.

    float Rg = planetRadius;
    float Rg2 = Rg * Rg;
    float Rt = shellRadius();
    float Rt2 = Rt * Rt;

    // Avoid numerical precision problems by choosing a first viewer
    // position just *above* the planet surface.
    float baseHeight = Rg * 1.0e-6f;

    unsigned int sampleCount = HeightSamples * ViewAngleSamples * SunAngleSamples;
    Vector4f* inscatter = new Vector4f[sampleCount];

    for (unsigned int i = 0; i < HeightSamples; ++i)
    {
        float w = float(i) / float(HeightSamples);
        float h = w * w * (Rt - Rg) + baseHeight;
        float r = Rg + h;
        float r2 = r * r;

        cout << "layer " << i << ", height=" << h << "km\n";

        Vector2f eye(0.0f, r);

        for (unsigned int j = 0; j < ViewAngleSamples; ++j)
        {
            float v = float(j) / float(ViewAngleSamples - 1);
            float mu = max(-1.0f, min(1.0f, toMu(v)));
            float cosTheta = mu;
            float sinTheta2 = 1.0f - cosTheta * cosTheta;
            float sinTheta = sqrt(sinTheta2);
            Vector2f view(sinTheta, cosTheta);

            float pathLength;
            float d = Rg2 - r2 * sinTheta2;
            if (d > 0.0f && -r * cosTheta - sqrt(d) > 0.0f)
            {
                // Ray hits the planet
                pathLength = -r * cosTheta - sqrt(Rg2 - r2 * sinTheta2);
            }
            else
            {
                // Ray hits the sky
                pathLength = -r * cosTheta + sqrt(Rt2 - r2 * sinTheta2);
            }

            for (unsigned int k = 0; k < SunAngleSamples; ++k)
            {
                float w = float(k) / float(SunAngleSamples - 1);
                float muS = toMuS(w);
                float cosPhi = muS;
                float sinPhi = sqrt(max(0.0f, 1.0f - cosPhi * cosPhi));
                Vector2f sun(sinPhi, cosPhi);

                float stepLength = pathLength / float(ScatteringIntegrationSteps);
                Vector2f step = view * stepLength;

                Vector3f rayleigh = Vector3f::Zero();
                float mie = 0.0f;

                for (unsigned int m = 0; m < ScatteringIntegrationSteps; ++m)
                {
                    Vector2f x = eye + step * m;
                    float distanceToViewer = stepLength * m;
                    float rx2 = x.squaredNorm();
                    float rx = sqrt(rx2);

                    // Compute the transmittance along the path to the viewer
                    Vector3f viewPathTransmittance = transmittance(r, mu, distanceToViewer, *this);

                    // Compute the cosine and sine of the angle between the
                    // sun direction and zenith at the current sample.
                    float c = x.dot(sun) / rx;
                    float s2 = 1.0f - c * c;

                    // Compute the transmittance along the path to the sun
                    // and the total transmittance t.
                    Vector3f t;
                    if (Rg2 - rx2 * s2 < 0.0f || -rx * c - sqrt(Rg2 - rx2 * s2) < 0.0f)
                    {
                        // Compute the distance through the atmosphere
                        // in the direction of the sun
                        float sunPathLength = -rx * c + sqrt(Rt2 - rx2 * s2);
                        Vector3f sunPathTransmittance = transmittance(rx, c, sunPathLength, *this);

                        t = viewPathTransmittance.cwiseProduct(sunPathTransmittance);
                    }
                    else
                    {
                        // Ray to sun intersects the planet; no inscattered
                        // light at this point.
                        t = Vector3f::Zero();
                    }

                    // Accumulate Rayleigh and Mie scattering
                    float hx = rx - Rg;
                    rayleigh += (exp(-hx / rayleighScaleHeight) * stepLength) * t;
                    mie += exp(-hx / mieScaleHeight) * stepLength * t.x();
                }

                unsigned int index = (i * ViewAngleSamples + j) * SunAngleSamples + k;
                inscatter[index] << rayleigh.cwiseProduct(rayleighCoeff),
                                    mie * mieCoeff;
                if (i == HeightSamples - 1 && k == 0)
                {
                    cout << acos(muS) * 180.0/M_PI << ", "
                         << acos(mu) * 180.0/M_PI << ", "
                         << inscatter[index].transpose() << endl;
                }

#if 0
                // Emit warnings about NaNs in scatter table
                if (isnan(rayleigh.x()))
                {
                    cout << "NaN in inscatter table at (" << k << ", " << j << ", " << i << ")\n";
                }
#endif
            }
        }
    }

    return inscatter;
}



void usage()
{
    cerr << "Usage: scattertable [options] <config file>\n";
    cerr << "   --output <filename> (or -o) : set filename of output image\n";
    cerr << "           (default is out.atm)\n";
    cerr << "   --scattersteps <value> (or -s)\n";
    cerr << "           set the number of integration steps for scattering\n";
}


/*
 * Theory:
 * Atmospheres are assumed to be composed of two different populations of
 * particles: Rayleigh scattering and Mie scattering. The density
 * of each population decreases exponentially with height above the planet
 * surface to a degree determined by a scale height:
 *
 *     density(height) = e^(-height/scaleHeight)
 *
 * Rayleigh scattering is wavelength dependent, with a fixed phase function.
 *
 * Mie scattering is wavelength independent, with a phase function determined
 * by a single parameter g (the asymmetry parameter). Mie scattering aerosols
 * may also be assigned wavelength dependent absorption coefficients.
 */

Vector3f computeRayleighCoeffs(const Vector3f& wavelengths)
{
    return wavelengths.array().pow(-4.0f);
}


void SetAtmosphereParameters(Atmosphere& atm, ParameterSet& params)
{
    atm.rayleighScaleHeight   = params["RayleighScaleHeight"];
    atm.rayleighCoeff.x()     = params["RayleighRed"];
    atm.rayleighCoeff.y()     = params["RayleighGreen"];
    atm.rayleighCoeff.z()     = params["RayleighBlue"];

    atm.mieScaleHeight        = params["MieScaleHeight"];
    atm.mieCoeff              = params["Mie"];

    atm.absorptionCoeff.x()   = params["AbsorbRed"];
    atm.absorptionCoeff.y()   = params["AbsorbGreen"];
    atm.absorptionCoeff.z()   = params["AbsorbBlue"];

    atm.planetRadius          = params["Radius"];
}


void SetDefaultParameters(ParameterSet& params)
{
    // Compute default rayleigh coefficients index of refraction and
    // molecular densities for Earth's atmosphere.
    Vector3f rayleighCoeff = RayleighScatteringCoeff(1.00027712f, 2.5470e25f);
    float km = 1000.0f;

    params["RayleighScaleHeight"] = 7.94;
    params["RayleighRed"]         = rayleighCoeff.x() * km;
    params["RayleighGreen"]       = rayleighCoeff.y() * km;
    params["RayleighBlue"]        = rayleighCoeff.z() * km;

    params["MieScaleHeight"]      = 1.2;
    params["Mie"]                 = 2.1e-6f * km;

    params["AbsorbRed"]           = 0.0;
    params["AbsorbGreen"]         = 0.0;
    params["AbsorbBlue"]          = 0.0;

    params["Radius"]              = 6378.0;
}


bool LoadParameterSet(ParameterSet& params, const string& filename)
{
    ifstream in(filename);

    if (!in.good())
    {
        cerr << "Error opening config file " << filename << endl;
        return false;
    }

    while (in.good())
    {
        string name;
        in >> name;

        double numValue = 0.0;
        in >> numValue;
        if (in.good())
        {
            params[name] = numValue;
        }
    }

    if (in.bad())
    {
        cerr << "Error in scene config file " << filename << endl;
        return false;
    }
    else
    {
        return true;
    }
}


string ConfigFileName;
string OutputFileName("out.atm");

bool parseCommandLine(int argc, char* argv[])
{
    int i = 1;
    int fileCount = 0;

    while (i < argc)
    {
        if (argv[i][0] == '-')
        {
            if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--scattersteps"))
            {
                if (i == argc - 1)
                    return false;

                if (sscanf(argv[i + 1], " %u", &ScatteringIntegrationSteps) != 1)
                    return false;
                i++;
            }
            else if (!strcmp(argv[i], "-o") || !strcmp(argv[i], "--output"))
            {
                if (i == argc - 1)
                    return false;

                OutputFileName = string(argv[i + 1]);
                i++;
            }
            else
            {
                return false;
            }
            i++;
        }
        else
        {
            if (fileCount == 0)
            {
                // input filename first
                ConfigFileName = string(argv[i]);
                fileCount++;
            }
            else
            {
                // more than one filenames on the command line is an error
                return false;
            }
            i++;
        }
    }

    return true;
}


union Uint16
{
    char bytes[2];
    uint16_t u;
};

union Uint32
{
    char bytes[4];
    uint32_t u;
};

union Float
{
    char bytes[4];
    float f;
};

union FloatInt
{
    float f;
    uint32_t u;
};


// Convert a single precision floating point value to half precision
uint16_t floatToHalf(float f)
{
    FloatInt fi;
    fi.f = f;

    auto signBit = uint16_t((fi.u & 0x80000000) >> 16);

    if (f > 65504.0f)
    {
        // overflow
        return 0x7c00;
    }
    if (f < -65504.0f)
    {
        // overflow
        return 0xfc00;
    }

    int exponent = int((fi.u >> 23) & 0xff) - 127 + 15;
    uint32_t significand = fi.u & 0x007fffff;

    if (exponent < -9)
    {
        // Value is too small even to represent as a subnormal
        return signBit;
    }
    if (exponent <= 0)
    {
        // Convert to a subnormal
        return signBit + (significand >> (13 - exponent));
    }
    else if (exponent + 127 - 15 == 0xff)
    {
        // Special values: infinities and NaNs
        if (significand == 0)
        {
            // Infinity
            return signBit + 0x7c00;
        }


            // NaN - preserve bits, but make sure that we don't
            // make the significand zero, as that would indicate
            // an infinity, not a NaN
            auto nanBits = uint16_t(significand >> 13);
            if (nanBits == 0)
            {
                nanBits = 1;
            }
            return signBit + 0x7c00 + nanBits;

    }
    else if (exponent > 30)
    {
        // Overflow; return infinity
        return signBit + 0x7c00;
    }
    else
    {
        // Normal value
        return signBit + (exponent << 10) +
               ((significand + 0x00001000) >> 13); // round
    }
}


struct DDSPixelFormat
{
    uint32_t dwSize{0};
    uint32_t dwFlags{0};
    uint32_t dwFourCC{0};
    uint32_t dwRGBBitCount{0};
    uint32_t dwRBitMask{0};
    uint32_t dwGBitMask{0};
    uint32_t dwBBitMask{0};
    uint32_t dwABitMask{0};
};


// Header for Microsoft DDS file format
struct DDSHeader
{
    DDSHeader()
    {
        fill(begin(dwReserved1), end(dwReserved1), 0);
    }

    static const uint32_t CAPS_COMPLEX = 0x000008;
    static const uint32_t CAPS_MIPMAP  = 0x400000;
    static const uint32_t CAPS_TEXTURE = 0x001000;

    static const uint32_t CAPS2_VOLUME = 0x200000;
    static const uint32_t CAPS2_CUBEMAP = 0x00000200;
    static const uint32_t CAPS2_CUBEMAP_POSITIVEX = 0x00000400;
    static const uint32_t CAPS2_CUBEMAP_NEGATIVEX = 0x00000800;
    static const uint32_t CAPS2_CUBEMAP_POSITIVEY = 0x00001000;
    static const uint32_t CAPS2_CUBEMAP_NEGATIVEY = 0x00002000;
    static const uint32_t CAPS2_CUBEMAP_POSITIVEZ = 0x00004000;
    static const uint32_t CAPS2_CUBEMAP_NEGATIVEZ = 0x00008000;

    static const uint32_t DDSD_CAPS      = 0x1;
    static const uint32_t DDSD_HEIGHT    = 0x2;
    static const uint32_t DDSD_WIDTH     = 0x4;
    static const uint32_t DDSD_PITCH     = 0x8;
    static const uint32_t DDSD_PIXELFORMAT = 0x1000;
    static const uint32_t DDSD_MIPMAPCOUNT = 0x20000;
    static const uint32_t DDSD_LINEARSIZE  = 0x80000;
    static const uint32_t DDSD_DEPTH       = 0x800000;

    static const uint32_t D3DFMT_A16B16G16R16 =   36;
    static const uint32_t D3DFMT_A16B16G16R16F = 113;
    static const uint32_t D3DFMT_DXT1 = 0x31545844;
    static const uint32_t D3DFMT_DXT3 = 0x33545844;
    static const uint32_t D3DFMT_DXT5 = 0x35545844;

    static const uint32_t FOURCC      = 0x04;

    uint32_t          dwSize{sizeof(DDSHeader)};
    uint32_t          dwFlags{DDSD_PIXELFORMAT};
    uint32_t          dwHeight{0};
    uint32_t          dwWidth{0};
    uint32_t          dwLinearSize{0};
    uint32_t          dwDepth{0};
    uint32_t          dwMipMapCount{0};
    uint32_t          dwReserved1[11];
    DDSPixelFormat    ddpf;
    uint32_t          dwCaps{0};
    uint32_t          dwCaps2{0};
    uint32_t          dwCaps3{0};
    uint32_t          dwCaps4{0};
    uint32_t          dwReserved2{0};

    void setTexture()
    {
        dwCaps |= CAPS_TEXTURE;
    }

    void setFourCC(uint32_t fcc)
    {
        dwFlags |= FOURCC;
        ddpf.dwFourCC = fcc;
    }

    void setMipMapLevels(uint32_t levels)
    {
        dwCaps |= (CAPS_COMPLEX | CAPS_MIPMAP);
        dwFlags |= DDSD_MIPMAPCOUNT;
        dwMipMapCount = levels;
    }

    void setDimensions(uint32_t width, uint32_t height)
    {
        dwFlags |= (DDSD_WIDTH | DDSD_HEIGHT);
        dwWidth = width;
        dwHeight = height;
    }

    void setVolumeDimensions(uint32_t width, uint32_t height, uint32_t depth)
    {
        dwCaps |= CAPS_COMPLEX;
        dwFlags |= (DDSD_WIDTH | DDSD_HEIGHT | DDSD_DEPTH);
        dwWidth = width;
        dwHeight = height;
        dwDepth = depth;
    }
};

static bool ByteSwapRequired = false;
static bool IsLittleEndian()
{
    Uint32 endiannessTest;
    endiannessTest.u = 0x01020304;
    return endiannessTest.bytes[0] == 0x04;
}


// Write out a 16-bit unsigned integer in little-endian order
static void WriteUint16(ostream& out, uint16_t u)
{
    static_assert(sizeof(u) == 2, "");

    Uint16 ub;
    ub.u = u;
    if (ByteSwapRequired)
    {
        swap(ub.bytes[0], ub.bytes[1]);
    }

    out.write(ub.bytes, sizeof(ub.bytes));
}


// Write out a 32-bit unsigned integer in little-endian order
static void WriteUint32(ostream& out, uint32_t u)
{
    static_assert(sizeof(u) == 4, "");

    Uint32 ub;
    ub.u = u;
    if (ByteSwapRequired)
    {
        swap(ub.bytes[0], ub.bytes[3]);
        swap(ub.bytes[1], ub.bytes[2]);
    }

    out.write(ub.bytes, sizeof(ub.bytes));
}


#if 0
// Write out a single precision floating point number
static void WriteFloat(ostream& out, float f)
{
    static_assert(sizeof(f) == 4, "");

    Float ub;
    ub.f = f;
    if (ByteSwapRequired)
    {
        swap(ub.bytes[0], ub.bytes[3]);
        swap(ub.bytes[1], ub.bytes[2]);
    }

    out.write(ub.bytes, sizeof(ub.bytes));
}
#endif


// Convert a single precision floating point value to half precision
// and write it out.
static void WriteHalfFloat(ostream& out, float f)
{
    WriteUint16(out, floatToHalf(f));
}


void WriteDDSHeader(ostream& out, const DDSHeader& dds)
{
    WriteUint32(out, dds.dwSize);
    WriteUint32(out, dds.dwFlags);
    WriteUint32(out, dds.dwHeight);
    WriteUint32(out, dds.dwWidth);
    WriteUint32(out, dds.dwLinearSize);
    WriteUint32(out, dds.dwDepth);
    WriteUint32(out, dds.dwMipMapCount);
    for (unsigned int i : dds.dwReserved1)
    {
        WriteUint32(out, i);
    }

    WriteUint32(out, dds.ddpf.dwSize);
    WriteUint32(out, dds.ddpf.dwFlags);
    WriteUint32(out, dds.ddpf.dwFourCC);
    WriteUint32(out, dds.ddpf.dwRGBBitCount);
    WriteUint32(out, dds.ddpf.dwRBitMask);
    WriteUint32(out, dds.ddpf.dwGBitMask);
    WriteUint32(out, dds.ddpf.dwBBitMask);
    WriteUint32(out, dds.ddpf.dwABitMask);

    WriteUint32(out, dds.dwCaps);
    WriteUint32(out, dds.dwCaps2);
    WriteUint32(out, dds.dwCaps3);
    WriteUint32(out, dds.dwCaps4);
    WriteUint32(out, dds.dwReserved2);
}


static void WriteInscatterTableDDS(ostream& out, Vector4f* inscatterTable)
{
    DDSHeader dds;
    dds.setTexture();
    dds.setFourCC(DDSHeader::D3DFMT_A16B16G16R16F);
    dds.setVolumeDimensions(SunAngleSamples, ViewAngleSamples, HeightSamples);
    //dds.setMipMapLevels();

    WriteDDSHeader(out, dds);

    unsigned int sampleCount = SunAngleSamples * ViewAngleSamples * HeightSamples;
    for (unsigned int i = 0; i < sampleCount; ++i)
    {
        const Vector4f& v = inscatterTable[i];
        WriteHalfFloat(out, v.x());
        WriteHalfFloat(out, v.y());
        WriteHalfFloat(out, v.z());
        WriteHalfFloat(out, v.w());
    }
}


static void WriteTransmittanceTableDDS(ostream& out, Vector3f* transmittanceTable)
{
    DDSHeader dds;
    dds.setTexture();
    dds.setFourCC(DDSHeader::D3DFMT_A16B16G16R16F);
    dds.setDimensions(ViewAngleSamples, HeightSamples);
    //dds.setMipMapLevels();

    WriteDDSHeader(out, dds);

    unsigned int sampleCount = ViewAngleSamples * HeightSamples;
    for (unsigned int i = 0; i < sampleCount; ++i)
    {
        const Vector3f& v = transmittanceTable[i];
        WriteHalfFloat(out, v.x());
        WriteHalfFloat(out, v.y());
        WriteHalfFloat(out, v.z());
        WriteHalfFloat(out, 0.0f);
    }
}



#if TEST_FLOAT_TO_HALF
int main(void)
{
    while (cin.good())
    {
        float f = 0.0f;
        cin >> f;
        cout << hex << floatToHalf(f) << endl;
    }
    return 0;
}
#endif

int main(int argc, char* argv[])
{
    bool commandLineOK = parseCommandLine(argc, argv);
    if (!commandLineOK || ConfigFileName.empty())
    {
        usage();
        exit(1);
    }

    ParameterSet params;
    SetDefaultParameters(params);
    if (!LoadParameterSet(params, ConfigFileName))
    {
        exit(1);
    }

    Atmosphere atmosphere;
    SetAtmosphereParameters(atmosphere, params);

    cout << "Planet radius: " << atmosphere.planetRadius << "km\n";
    cout << "Rayleigh scale height: " << atmosphere.rayleighScaleHeight << "km\n";
    cout << "Rayleigh coeff: " << atmosphere.rayleighCoeff.transpose() << "m^-1\n";

    cout << "Mie scale height: " << atmosphere.mieScaleHeight << "km\n";
    cout << "Mie coeff: " << atmosphere.mieCoeff << "m^-1\n";
    cout << "Absorption coeff: " << atmosphere.absorptionCoeff.transpose() << "m^-1\n";
    cout << "Using " << ScatteringIntegrationSteps << " integration steps.\n";

    cout << "Generating transmittance table (" << ViewAngleSamples << "x"
         << HeightSamples << ")...\n";
    Vector3f* transmittanceTable = atmosphere.computeTransmittanceTable();

    cout << "Generating inscatter table (" << SunAngleSamples << "x"
         << ViewAngleSamples << "x" << HeightSamples << ")...\n";

    Vector4f* inscatterTable = atmosphere.computeInscatterTable();

    ByteSwapRequired = !IsLittleEndian();

#if 0
    // Write tables in a single file
    ofstream out(OutputFileName, ostream::binary);

    // Header
    out.write("atmscatr", 8);

    // Version
    WriteUint32(out, 1u);

    // Scattering parameters
    WriteFloat(out, atmosphere.rayleighScaleHeight);
    WriteFloat(out, atmosphere.rayleighCoeff.x());
    WriteFloat(out, atmosphere.rayleighCoeff.y());
    WriteFloat(out, atmosphere.rayleighCoeff.z());
    WriteFloat(out, atmosphere.mieScaleHeight);
    WriteFloat(out, atmosphere.mieCoeff);
    WriteFloat(out, atmosphere.mieAsymmetry);
    WriteFloat(out, atmosphere.absorptionCoeff.x());
    WriteFloat(out, atmosphere.absorptionCoeff.y());
    WriteFloat(out, atmosphere.absorptionCoeff.z());
    WriteFloat(out, atmosphere.planetRadius);

    // Transmittance table dimensions
    WriteUint32(out, ViewAngleSamples);
    WriteUint32(out, HeightSamples);

    // Inscatter table dimensions
    WriteUint32(out, SunAngleSamples);
    WriteUint32(out, ViewAngleSamples);
    WriteUint32(out, HeightSamples);

    unsigned int transmittanceTableSamples = ViewAngleSamples * HeightSamples;
    unsigned int inscatterTableSamples = SunAngleSamples * ViewAngleSamples *
        HeightSamples;

    for (unsigned int i = 0; i < transmittanceTableSamples; ++i)
    {
        Vector3f v = transmittanceTable[i];
        WriteFloat(out, v.x());
        WriteFloat(out, v.y());
        WriteFloat(out, v.z());
    }

    for (unsigned int i = 0; i < inscatterTableSamples; ++i)
    {
        Vector4f v = inscatterTable[i];
        WriteFloat(out, v.x());
        WriteFloat(out, v.y());
        WriteFloat(out, v.z());
        WriteFloat(out, v.w());
    }

    out.close();
#else
    // Write tables as separate DDS files
    ofstream transmittanceOut("transmittance.dds", ostream::binary);
    WriteTransmittanceTableDDS(transmittanceOut, transmittanceTable);
    transmittanceOut.close();

    ofstream inscatterOut("inscatter.dds", ostream::binary);
    WriteInscatterTableDDS(inscatterOut, inscatterTable);
    inscatterOut.close();
#endif

    return 0;
}
