// testint.C #include #include #include #include #include #include "nied31.h" #include "vector.H" #include "matrix.H" namespace xrml { static unsigned idx; Vec3 uniformdir(double xi_1, double xi_2) { double phi, cos_phi, sin_phi, cos_theta, sin_theta; phi = 2. * M_PI * xi_1; cos_phi = cos(phi); sin_phi = sin(phi); cos_theta = 1.0-xi_2; sin_theta = sqrt(1.-cos_theta*cos_theta); return Vec3( cos_phi * sin_theta, sin_phi * sin_theta, cos_theta ); } static Vec3 IdealReflectedDirection(const Vec3& inDir) { return Vec3(-inDir.x, -inDir.y, inDir.z); } class testfunc { public: virtual double f(const Vec3& inDir, const Vec3& outDir) { return 0.; } virtual double integral(const Vec3& inDir) { return 0.; } }; class testf1: public testfunc { public: // constant function double f(const Vec3& inDir, const Vec3& outDir) { return outDir.z > 0.? 1. : 0.; } double integral(const Vec3& inDir) { return 2. * M_PI; } }; class testf2: public testfunc { public: // cos(outDir, normal) double f(const Vec3& inDir, const Vec3& outDir) { return outDir.z > 0. ? outDir.z : 0.; } double integral(const Vec3& inDir) { return M_PI; } }; class testf3: public testfunc { public: double sharpness; // Phong lobe with sharpness given below testf3(double s = 10.) { sharpness = s; } double f(const Vec3& inDir, const Vec3& outDir) { double cosa = IdealReflectedDirection(inDir) & outDir; return (cosa <= 0.) ? 0. /* angle >= 90deg */ : pow(cosa, sharpness); } double integral(const Vec3& inDir) { return (2 * M_PI) / (sharpness + 1); } }; class bsdf { public: virtual Vec3 sample(const Vec3& inDir, double xi_1, double xi_2) { abort(); return Vec3(0,0,0); } virtual float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { abort(); return 0.; } virtual float albedo(const Vec3& inDir) {abort(); return 0.; } }; class diffuse: public bsdf { public: Vec3 sample(const Vec3& inDir, double xi1, double xi2) { double phi = 2. * M_PI * xi1; double cos_phi = cos(phi); double sin_phi = sin(phi); double cos_theta = sqrt(xi2); double sin_theta = sqrt(1.-xi2); // generate outDir on the same side of the surface // as inDir. return Vec3(cos_phi * sin_theta, sin_phi * sin_theta, cos_theta * (inDir.z >= 0. ? 1. : -1.)); } float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { if (pdf) *pdf = 0.; if ((inDir.z >= 0.) != (outDir.z >= 0)) return 0.; // refracted ray. Also pdf is 0. if (pdf) *pdf = outDir.z / M_PI; return 1. / M_PI; } float albedo(const Vec3& inDir) { return 1.; } }; class phong: public bsdf { public: double sharpness; phong(double sharp = 10.) { sharpness = sharp; } Vec3 sample(const Vec3& inDir, double xi1, double xi2) { // rotation transforming the Z axis into // the ideal reflected direction. // The axis X is chosen to lay in the surface // tangent plane. Vec3 Z = IdealReflectedDirection(inDir); double zz = sqrt(1 - Z.z*Z.z); Vec3 X = (zz < EPSILON) // normal incidence ? Vec3(1.,0.,0.) : Vec3(Z.y/zz, -Z.x/zz, 0.); Vec3 Y = Z ^ X; Mat3 rot(X,Y,Z); double phi = 2. * M_PI * xi1; double cos_phi = cos(phi); double sin_phi = sin(phi); double cos_theta = pow(xi2, 1./(sharpness+1.)); double sin_theta = sqrt(1. - cos_theta*cos_theta); return Vec3(cos_phi * sin_theta, sin_phi * sin_theta, cos_theta) * rot; } float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { if (pdf) *pdf = 0.; double cosa = IdealReflectedDirection(inDir) & outDir; if (cosa <= 0.) // angle >= 90 degrees // the sampling algorithm above cannot yield such // directions, so also the pdf is 0. return 0.; double val = pow(cosa, sharpness); if (pdf) *pdf = val * (sharpness+1.) / (2. * M_PI); if ((outDir.z > 0.) != (inDir.z > 0.)) // refracted direction // pdf is not 0: the sampling method above can yield rays // leaving on the wrong side of the surface. return 0.; return val * (sharpness+2.) / (2. * M_PI); } // returns approximate albedo of modified Phong BRDF: we assume that // the cosine of the angle between outgoing direction and surface normal // remains constant over the Phong lobe. This is a good approximation for // sharp distributions. float albedo(const Vec3& inDir) { return fabs(inDir.z); } }; class ward: public bsdf { // G. Ward, "Modeling and measuring anisotropic reflectance", SIGGRAPH'92 public: double standardDeviationX, standardDeviationY; ward(double stddevX = 0.1, double stddevY = 0.1) { standardDeviationX = stddevX; standardDeviationY = stddevY; } // Samples outgoing direction for given incident directon and random // numbers in the range [0,1]. // pdf is proportional to Wards eliptic Gaussian Vec3 sample(const Vec3& inDir, double xi_1, double xi_2) { double r = (xi_1 > EPSILON*EPSILON) ? sqrt(-log(xi_1)) : HUGE; xi_2 *= 2.*M_PI; double x = standardDeviationX * r * cos(xi_2); double y = standardDeviationY * r * sin(xi_2); double z = 1. / sqrt(x*x + y*y + 1.); // choose z so h will be normalised x *= z; y *= z; Vec3 h = Vec3(x, y, z); // halfway vector (microfacet normal) return h * (2. * (h & inDir)) - inDir; // mirror inDir w.r.t. h } // evaluates Wards SIGGRAPH'92 scattering funtion for given incident and // outgoing direction. If 'pdf' is not nil, also computes the probability that // the routine sample() above would pick outDir. float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { if (pdf) *pdf = 0.; // initialise Vec3 h = (Vec3)inDir + (Vec3)outDir; // halfway vector (unnormalised). double xfac = (h.x / (h.z * standardDeviationX)); // normalisation of h doesn't matter. double yfac = (h.y / (h.z * standardDeviationY)); double val = exp( - (xfac*xfac + yfac*yfac) ); // Wards eliptic Gaussian // For normalised h, one division more, by the square length of the unnormalised // halfway vector, would be necessary. double N = M_PI * standardDeviationX * standardDeviationY; if (pdf) *pdf = val * (h & outDir) / (h.z*h.z*h.z) / N; if ((inDir.z >= 0) != (outDir.z >= 0.)) // outDir and inDir are on different sides of the surface // pdf is not 0: the sampling method above can yield rays leaving // on the wrong side of the surface! return 0.; double normalisation = sqrt( inDir.z * outDir.z ) * 4 * N; if (normalisation < EPSILON) // vector lies in plane (inDir.z == 0 || outDir.z == 0) or illegal parameter values (standardDeviationX == 0 || standardDeviationY == 0) return 0.; return val / normalisation; } // Computes the integral over all outgoing direction of Wards scattering // function times the outgoing cosine. This is the fraction of energy incident // from 'inDir' that gets reflected. // This approximation for the albedo is good for small standardDeviationX, // standardDeviationY and for normal incidence. For grazing angles, the error can be // large because part of the lobe will be under the surface. float albedo(const Vec3& inDir) { return fabs(inDir.z); } }; class schlick: public bsdf { // C. Schlick, "An inexpensive BRDF model for physically based rendering", // Computer Graphics Forum Vol 13, nr 3 (Proceedings of EuroGraphics 1994), p 233 // Modified so that the albedo will not depend on roughness and with proper // pdf. public: double roughness; double isotropy; protected: bool parmschecked; inline void checkparms(void) { if (roughness < EPSILON || isotropy < EPSILON) { cerr << "Invalid parameters\n"; } parmschecked = true; } public: schlick(double roughn =1., double isot =1.) { roughness = roughn; isotropy = isot; parmschecked = false; } Vec3 sample(const Vec3& inDir, double xi_1, double xi_2) { if (!parmschecked) checkparms(); // zenith angle double c2 = xi_1 / (roughness + (1.-roughness) * xi_1); double s = sqrt(1. - c2); // determine in which quadrant the azimuthal angle must be if (xi_2 > 1.-EPSILON) xi_2 = 1.-EPSILON; xi_2 *= 4.; double fxi2 = floor(xi_2); int quadrant = (int)fxi2; // make random nr in the rand [0,1) again // mirrored in quadrant 1 and 3, so result will change // continuously as a function of xi2. xi_2 = xi_2 - fxi2; if (quadrant&1) xi_2 = 1. - xi_2; // azimuth angle in first quadrant double p2 = isotropy * isotropy; double b2 = xi_2 * xi_2; double psi = 0.5*M_PI * sqrt( p2 * b2 / (1. - (1.-p2) * b2) ); Vec3 h(cos(psi)*s, sin(psi)*s, sqrt(c2)); switch (quadrant) { case 0: break; case 1: h.x = -h.x; break; case 2: h.x = -h.x; h.y = -h.y; break; case 3: h.y = -h.y; break; } return h * (2. * (h & inDir)) - inDir; // mirror inDir w.r.t. h } float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { if (!parmschecked) checkparms(); // halfway vector (microfacet normal) Vec3 h = (Vec3)inDir + (Vec3)outDir; double H = (h & h); if (H < EPSILON) { // inDir == -outDir, rarely results from the sampling algorithm above. if (pdf) *pdf = 1.; // avoid divisions by zero. return 0.; } H = sqrt(H); h *= (1./H); // zenith angle dependence double t2 = h.z*h.z; // t^2 double Z = (1-t2) + roughness * t2; Z = roughness / (Z*Z); // azimuth angle dependence double w2 = (t2 > 1.-EPSILON) ? 0. : h.x*h.x / (1. - t2) ; // w^2 double A = sqrt( isotropy / (isotropy*isotropy * (1-w2) + w2) ); if (pdf) { // The pdf corresponding to Schlicks sampling formula (formula 29-right // on page C-244) is NOT A(w) dw / sqrt(1-w^2) but something completely // different !!! It is not even particularly suited to integrate Schlicks // own BRDF when anisotropic !!! double x = (2./M_PI) * ((w2=1.) ? 0. : acos(sqrt(w2))); double p = isotropy; double p2 = p*p; double A1 = 1 + (1-p2)/p2 * x*x; *pdf = fabs(((Vec3)h & (Vec3)outDir) * h.z) / (M_PI * H*H) * Z / (p * A1 * sqrt(A1)); } double vi = inDir.z; double vo = outDir.z; if (fabs(vi) < EPSILON || fabs(vo) < EPSILON || (vi>0.) != (vo>0.)) // incoming or outgoing direction in surface tangent plane or // they are directions on different sides of the surface // pdf is not zero: the sampling algorithm above can yield such // directions. return 0.; double Gi = vi / (roughness - roughness * vi + vi); double Go = vo / (roughness - roughness * vo + vo); return (Gi * Go * A * Z + (1. - Gi * Go)) / (4.*M_PI * vi * vo); } float albedo(const Vec3& inDir) { // The value below is an approximation only. return fabs(inDir.z); } }; class ashikmin: public bsdf { protected: float nu, nv; public: ashikmin(double _nu =1., double _nv =1.) { nu = _nu; nv = _nv; } float albedo(const Vec3& inDir) { return fabs(inDir.z); } Vec3 sample(const Vec3& inDir, float xi1, float xi2) { double theta = atan( sqrt( (nu + 1) / (nv + 1) ) * tan( M_PI * xi1 / 2 ) ); double sinTheta = sin(theta); double cosTheta = cos(theta); double cosDelta = pow( (1 - xi2), 1. / (nu * cosTheta * cosTheta + nv * sinTheta * sinTheta + 1) ); double sinDelta = sqrt(1 - cosDelta * cosDelta); Vec3 h = Vec3( sinTheta * cosDelta, sinTheta * sinDelta, cosTheta ); return Vec3(-inDir + h * 2 * (inDir & h)); } float eval(const Vec3& inDir,const Vec3& outDir, float *pdf) { Vec3 h = Vec3(inDir + outDir).normalize(); double hu = h.x; double hv = h.y; double hn = h.z; double hk1 = h & inDir; double hk2 = h & outDir; double nk1 = inDir.z; double nk2 = outDir.z; if(hn < EPSILON) return(1.); if(hk2 < EPSILON) return(1.); if(hk1 < EPSILON) return(1.); double c = sqrt( (nu + 1) * (nv + 1) ) / (8 * M_PI); double max = (nk1 > nk2) ? nk1 : nk2; double p = pow(hn, (nu*hu*hu + nv*hv*hv) / (1 - hn*hn) ); if(pdf) *pdf = (float)(c * p / hk1); return( (float)((c * p) / (hk2 * max)) ); } }; class neumann : public bsdf { protected: float sharpness; public: neumann(double s=0.) { sharpness = s; } static Vec3 Mirror(Vec3 dir) { return Vec3(-dir.x, -dir.y, dir.z); } float albedo(const Vec3& inDir) { return fabs(inDir.z); } Vec3 sample(const Vec3& inDir, float xi1, float xi2) { // uses the sample sampling as (and pdf) as in: // // "Using the Modified Phong Reflectance Model for Physically Based Rendering" // Eric P. Lafortune, Yves D. Willems // Technical Report CW 197, November 1994 // (Department of Computing Science, K.U.Leuven) // float u = pow(xi1, 2. / (sharpness + 1)); float v = sqrt(1 - u*u); float phi = 2 * M_PI * xi2; Vec3 dir = Vec3(v * cos(phi), v * sin(phi), u); return Mirror(dir); } float eval(const Vec3& inDir, const Vec3& outDir, float *pdf) { if (pdf) *pdf = 0.; double cosa = Mirror(inDir) & outDir; if (cosa <= 0.) // angle >= 90 degrees // pdf is 0: such outgoing rays cannot result from the sampling method // above. return 0.; double val = pow(cosa, sharpness); double cos_max = (inDir.z > outDir.z) ? inDir.z : outDir.z; if (pdf) *pdf = val * (sharpness+1.) / (2. * M_PI); if ((outDir.z > 0.) != (inDir.z > 0.)) // refracted direction // pdf is not 0: the sampling method above can yield rays // leaving the wrong side of the surface! return 0.; return val * (sharpness+2.) / (2. * M_PI * cos_max); } }; static int nrsamples = 100000000; // computes albedo by means of uniform random sampling and compares the result // with what we think it should be. // Monte Carlo integration: generate random samples with certain pdf, compute // average ratio of function value (here: bsdf x cos) over sample pdf. double TestAlbedo(class bsdf* sampling, class bsdf *s, Vec3 inDir) { int n = 1; double sum = 0., ratio = 1.; while (n <= nrsamples) { unsigned *zeta = Nied31(idx++); Vec3 outDir = sampling->sample(inDir, (double)zeta[0]*RECIP, (double)zeta[1]*RECIP); float pdf; sampling->eval(inDir, outDir, &pdf); float bsdf = s->eval(inDir, outDir, 0); sum += bsdf * outDir.z / pdf; // outDir.z = cos(outDir, normal=Z) if ((n>0 && floor(log10((double)n)) != floor(log10((double)n-1))) || n%1000000 == 0) fprintf(stderr, "%8d dirs: albedo = %f (verhouding = %f)\n", n, sum / (double)n , ratio = (sum / (double)n) / s->albedo(inDir) ); n++; } return ratio; } double TestSampling(class bsdf *s, Vec3 inDir, class testfunc *test) { int n = 1; double sum = 0., norm = 1.; while (n <= nrsamples) { unsigned *zeta = Nied31(idx++); Vec3 outDir = s->sample(inDir, (double)zeta[0]*RECIP, (double)zeta[1]*RECIP); float pdf; s->eval(inDir, outDir, &pdf); sum += test->f(inDir, outDir) / pdf; if ((n>0 && floor(log10((double)n)) != floor(log10((double)n-1))) || n%1000000 == 0) fprintf(stderr, "%8d dirs: integral = %f (verhouding = %f)\n", n, sum / (double)n , norm = (sum / (double)n) / test->integral(inDir) ); n++; } return norm; } // evaluates bsdf and pdf for directoins returns by s->sample() and writes out the // result in gnuplot format void PlotBsdf(class bsdf *s, Vec3 inDir) { int divs; fprintf(stderr, "Geef fijnheid (geheel getal, bvb 10)\n"); scanf("%d", &divs); int i, j; printf("# theta phi bsdf inDir = (%g,%g,%g)\n", inDir.x, inDir.y, inDir.z); for (i=0; i<=divs; i++) { for (j=0; j<=divs; j++) { Vec3 outDir = s->sample(inDir, (double)j/(double)divs, (double)i/(double)divs); float pdf, bsdf = s->eval(inDir, outDir, &pdf); printf("%f %f %f %f %f %f %f %f %f %f %f %f\n", outDir.x * bsdf, outDir.y * bsdf, outDir.z * bsdf, outDir.x * pdf, outDir.y * pdf, outDir.z * pdf, outDir.x * bsdf * outDir.z, outDir.y * bsdf * outDir.z, outDir.z * bsdf * outDir.z, outDir.x * pdf / outDir.z, outDir.y * pdf / outDir.z, pdf); } printf("\n"); } } Vec3 ChooseIndir(void) { Vec3 inDir; fprintf(stderr, "Geef inDir (x,y en z, z is volgens de normaal):\n"); scanf("%f %f %f", &inDir.x, &inDir.y, &inDir.z); inDir.normalize(); return inDir; } class testfunc* ChooseTestf1(void) { return new testf1; } class testfunc* ChooseTestf2(void) { return new testf2; } class testfunc* ChooseTestf3(void) { double sharpness; fprintf(stderr, "Enter sharpness:\n"); scanf("%lf", &sharpness); return new testf3(sharpness); } class testfunc* ChooseTestfunc(void) { fprintf(stderr, "Welke testfunctie?\n"); fprintf(stderr, "1 = constante functie\n"); fprintf(stderr, "2 = cosinus tussen uitgaande hoek en normaal (= diffuus model)\n"); fprintf(stderr, "3 = Phong lobe\n"); int which; scanf("%d", &which); switch (which) { case 1: return ChooseTestf1(); case 2: return ChooseTestf2(); case 3: return ChooseTestf3(); default: fprintf(stderr, "No such test function.\n"); exit(0); } return 0; } class bsdf* ChooseDiffuse(void) { return new diffuse; } class bsdf* ChoosePhong(void) { double sharpness; fprintf(stderr, "Enter sharpness:\n"); scanf("%lf", &sharpness); return new phong(sharpness); } class bsdf* ChooseWard(void) { fprintf(stderr, "Enter standard deviation X and Y:\n"); double stddevX, stddevY; scanf("%lf %lf", &stddevX, &stddevY); return new ward(stddevX, stddevY); } class bsdf* ChooseSchlick(void) { fprintf(stderr, "Enter roughness and isotropy:\n"); double roughness, isotropy; scanf("%lf %lf", &roughness, &isotropy); return new schlick(roughness, isotropy); } class bsdf* ChooseAshikmin(void) { fprintf(stderr, "Enter nu and nv:\n"); double nu, nv; scanf("%lf %lf", &nu, &nv); return new ashikmin(nu, nv); } class bsdf* ChooseNeumann(void) { fprintf(stderr, "Enter sharpness:\n"); double s; scanf("%lf", &s); return new neumann(s); } class bsdf *ChooseBsdf(char *question) { fprintf(stderr, "%s\n", question); fprintf(stderr, "1 = diffuse\n"); fprintf(stderr, "2 = Phong\n"); fprintf(stderr, "3 = Ward\n"); fprintf(stderr, "4 = Schlick\n"); fprintf(stderr, "5 = Ashikmin\n"); fprintf(stderr, "6 = Neumann\n"); int which; scanf("%d", &which); switch (which) { case 1: return ChooseDiffuse(); break; case 2: return ChoosePhong(); break; case 3: return ChooseWard(); break; case 4: return ChooseSchlick(); break; case 5: return ChooseAshikmin(); break; case 6: return ChooseNeumann(); break; default: fprintf(stderr, "Ongeldige keuze.\n"); exit(-1); } return 0; } void do_bla(void) { } } using namespace xrml; int main(int argc, char **argv) { char *progname = argv[0]; fprintf(stderr, "progname = '%s'\n", progname); idx = 1; fprintf(stderr, "Seeding random nr generator with %d\n", idx); if (strcmp(argv[0], "testalbedo") == 0) { TestAlbedo(ChooseBsdf("Welke sampling methode"), ChooseBsdf("Welke BSDF"), ChooseIndir()); } else if (strcmp(argv[0], "plotbsdf") == 0) { PlotBsdf(ChooseBsdf("Welke BSDF"), ChooseIndir()); } else if (strcmp(argv[0], "testsampling") == 0) { TestSampling(ChooseBsdf("Welke BSDF"), ChooseIndir(), ChooseTestfunc()); } else { do_bla(); } return 0; }