public override EmitterSample SampleRay(Vector2 primaryPos, Vector2 primaryDir) { var posSample = SampleArea(primaryPos); // Transform primary to cosine hemisphere (z is up) // We add one to the exponent, to importance sample the cosine term from the jacobian also var local = SampleWarp.ToCosineLobe(exponent + 1, primaryDir); // Transform to world space direction var normal = posSample.Point.ShadingNormal; Vector3 tangent, binormal; ShadingSpace.ComputeBasisVectors(normal, out tangent, out binormal); Vector3 dir = local.Direction.Z * normal + local.Direction.X * tangent + local.Direction.Y * binormal; float cosine = local.Direction.Z; var weight = radiance * MathF.Pow(cosine, exponent + 1) * normalizationFactor; return(new EmitterSample { Point = posSample.Point, Direction = dir, Pdf = local.Pdf * posSample.Pdf, Weight = weight / posSample.Pdf / local.Pdf }); }
public override (Ray, RgbColor, float) SampleRay(Vector2 primaryPos, Vector2 primaryDir) { // Sample a direction from the scene to the background var dirSample = SampleDirection(primaryDir); // Sample a point on the unit disc var unitDiscPoint = SampleWarp.ToConcentricDisc(primaryPos); // And transform it to the scene spanning disc orthogonal to the selected direction Vector3 tangent, binormal; ShadingSpace.ComputeBasisVectors(dirSample.Direction, out tangent, out binormal); var pos = SceneCenter + SceneRadius * (dirSample.Direction // offset outside of the scene + tangent * unitDiscPoint.X // remap unit disc x coordinate + binormal * unitDiscPoint.Y); // remap unit disc y coordinate // Compute the pdf: uniform sampling of a disc with radius "SceneRadius" float discJacobian = SampleWarp.ToConcentricDiscJacobian(); float posPdf = discJacobian / (SceneRadius * SceneRadius); // Compute the final result var ray = new Ray { Origin = pos, Direction = -dirSample.Direction, MinDistance = 0 }; var weight = dirSample.Weight / posPdf; var pdf = posPdf * dirSample.Pdf; return(ray, weight, pdf); }
float ComputeOneDir(Vector3 outDir, Vector3 inDir) { // Compute the half vector float eta = ShadingSpace.CosTheta(outDir) > 0 ? (insideIOR / outsideIOR) : (outsideIOR / insideIOR); Vector3 wh = outDir + inDir * eta; if (wh == Vector3.Zero) { return(0); // Prevent NaN if outDir and inDir exactly align } wh = Vector3.Normalize(wh); if (Vector3.Dot(outDir, wh) * Vector3.Dot(inDir, wh) > 0) { return(0); } // Compute change of variables _dwh\_dinDir_ for microfacet transmission float sqrtDenom = Vector3.Dot(outDir, wh) + eta * Vector3.Dot(inDir, wh); if (sqrtDenom == 0) { return(0); // Prevent NaN in corner case } float dwh_dinDir = Math.Abs((eta * eta * Vector3.Dot(inDir, wh)) / (sqrtDenom * sqrtDenom)); float result = distribution.Pdf(outDir, wh) * dwh_dinDir; Debug.Assert(float.IsFinite(result)); return(result); }
/// <summary> /// Creates a cone oriented such that it connects two points like an arrow tip /// </summary> /// <param name="baseCenter">The point in the center of the cone's base</param> /// <param name="tip">The position of the tip in world space</param> /// <param name="radius">Radius at the base of the cone</param> /// <param name="numSegments">Number of triangles used to build the side surface</param> public static Mesh MakeCone(Vector3 baseCenter, Vector3 tip, float radius, int numSegments) { ShadingSpace.ComputeBasisVectors(Vector3.Normalize(tip - baseCenter), out var tan, out var binorm); List <Vector3> vertices = new(); List <int> indices = new(); vertices.Add(tip); for (int i = 0; i < numSegments; ++i) { float angle = i * 2 * MathF.PI / numSegments; float y = MathF.Sin(angle) * radius; float x = MathF.Cos(angle) * radius; vertices.Add(baseCenter + tan * x + binorm * y); indices.AddRange(new List <int>() { 0, i, (i == numSegments - 1) ? 1 : (i + 1) }); } Mesh result = new(vertices.ToArray(), indices.ToArray()); return(result); }
public (float, float) Pdf(Vector3 outDir, Vector3 inDir, bool isOnLightSubpath) { if (ShadingSpace.SameHemisphere(outDir, inDir)) { return(0, 0); } return(ComputeOneDir(outDir, inDir), ComputeOneDir(inDir, outDir)); }
public void WorldToShade_ShouldBeXY() { var normal = new Vector3(1, 1, 0); var worldDir = new Vector3(0, 0, 1); var shadeDir = ShadingSpace.WorldToShading(normal, worldDir); Assert.Equal(0.0f, shadeDir.Z); }
public void WorldToShade_ShouldBeZAxis() { var normal = Vector3.Normalize(new Vector3(1, 1, 0)); var worldDir = Vector3.Normalize(new Vector3(-1, -1, 0)); var shadeDir = ShadingSpace.WorldToShading(normal, worldDir); Assert.Equal(0.0f, shadeDir.X); Assert.Equal(0.0f, shadeDir.Y); Assert.Equal(-1.0f, shadeDir.Z, 4); }
public void WorldToShade_AndBack_ShouldBeOriginalNormalized() { var normal = Vector3.Normalize(new Vector3(1, 5, 0)); var worldDir = Vector3.Normalize(new Vector3(1, 6, 2)); var shadeDir = ShadingSpace.WorldToShading(normal, worldDir); var worldDir2 = ShadingSpace.ShadingToWorld(normal, shadeDir); Assert.Equal(worldDir.X / worldDir.Length(), worldDir2.X, 4); Assert.Equal(worldDir.Y / worldDir.Length(), worldDir2.Y, 4); Assert.Equal(worldDir.Z / worldDir.Length(), worldDir2.Z, 4); }
/// <summary> /// Warps the given primary sample to follow the pdf computed by <see cref="Pdf(Vector3, Vector3)"/>. /// </summary> /// <returns>The direction that corresponds to the given primary sample.</returns> public Vector3 Sample(Vector3 outDir, Vector2 primary) { bool flip = ShadingSpace.CosTheta(outDir) < 0; var wh = TrowbridgeReitzSample(flip ? -outDir : outDir, AlphaX, AlphaY, primary.X, primary.Y); if (flip) { wh = -wh; } return(wh); }
/// <summary> /// Computes the ratio of self-masked area to visible area. Used by <see cref="MaskingShadowing(Vector3)"/>. /// </summary> /// <param name="normal">Normal of the microfacets, in shading space.</param> /// <returns>Ratio of self-masked area to visible area.</returns> public float MaskingRatio(Vector3 normal) { float absTanTheta = MathF.Abs(ShadingSpace.TanTheta(normal)); if (float.IsInfinity(absTanTheta)) { return(0); } float alpha = MathF.Sqrt(ShadingSpace.CosPhiSqr(normal) * AlphaX * AlphaX + ShadingSpace.SinPhiSqr(normal) * AlphaY * AlphaY); float alpha2Tan2Theta = alpha * absTanTheta * (alpha * absTanTheta); return((-1 + MathF.Sqrt(1 + alpha2Tan2Theta)) / 2); }
public override (Vector2, Vector2) SampleRayInverse(Vector3 dir, Vector3 pos) { var primaryDir = SampleDirectionInverse(-dir); // Project the point onto the plane with normal "dir" Vector3 tangent, binormal; ShadingSpace.ComputeBasisVectors(-dir, out tangent, out binormal); var offset = pos - SceneCenter; float x = Vector3.Dot(offset, tangent) / SceneRadius; float y = Vector3.Dot(offset, binormal) / SceneRadius; var primaryPos = SampleWarp.FromConcentricDisc(new(x, y)); return(primaryPos, primaryDir); }
public override (Vector2, Vector2) SampleRayInverse(SurfacePoint point, Vector3 direction) { var posPrimary = SampleAreaInverse(point); // Transform from world space to sampling space var normal = point.ShadingNormal; Vector3 tangent, binormal; ShadingSpace.ComputeBasisVectors(normal, out tangent, out binormal); float z = Vector3.Dot(normal, direction); float x = Vector3.Dot(tangent, direction); float y = Vector3.Dot(binormal, direction); var dirPrimary = SampleWarp.FromCosineLobe(exponent + 1, new(x, y, z)); return(posPrimary, dirPrimary); }
/// <summary> /// Computes the distribution of microfacets with the given normal. /// </summary> /// <param name="normal">The normal vector of the microfacets, in shading space.</param> /// <returns>The fraction of microfacets that are oriented with the given normal.</returns> public float NormalDistribution(Vector3 normal) { float tan2Theta = ShadingSpace.TanThetaSqr(normal); if (float.IsInfinity(tan2Theta)) { return(0); } float cos4Theta = ShadingSpace.CosThetaSqr(normal) * ShadingSpace.CosThetaSqr(normal); float e = tan2Theta * ( ShadingSpace.CosPhiSqr(normal) / (AlphaX * AlphaX) + ShadingSpace.SinPhiSqr(normal) / (AlphaY * AlphaY) ); return(1 / (MathF.PI * AlphaX * AlphaY * cos4Theta * (1 + e) * (1 + e))); }
public Vector3?Sample(Vector3 outDir, bool isOnLightSubpath, Vector2 primarySample) { if (outDir.Z == 0) { return(null); } Vector3 wh = distribution.Sample(outDir, primarySample); if (Vector3.Dot(outDir, wh) < 0) { return(null); } float eta = ShadingSpace.CosTheta(outDir) > 0 ? (outsideIOR / insideIOR) : (insideIOR / outsideIOR); var inDir = ShadingSpace.Refract(outDir, wh, eta); return(inDir); }
public RgbColor Evaluate(Vector3 outDir, Vector3 inDir, bool isOnLightSubpath) { if (ShadingSpace.SameHemisphere(outDir, inDir)) { return(RgbColor.Black); // transmission only } float cosThetaO = ShadingSpace.CosTheta(outDir); float cosThetaI = ShadingSpace.CosTheta(inDir); if (cosThetaI == 0 || cosThetaO == 0) { return(RgbColor.Black); } // Compute the half vector float eta = ShadingSpace.CosTheta(outDir) > 0 ? (insideIOR / outsideIOR) : (outsideIOR / insideIOR); Vector3 wh = Vector3.Normalize(outDir + inDir * eta); if (ShadingSpace.CosTheta(wh) < 0) { wh = -wh; } if (Vector3.Dot(outDir, wh) * Vector3.Dot(inDir, wh) > 0) { return(RgbColor.Black); } var F = new RgbColor(FresnelDielectric.Evaluate(Vector3.Dot(outDir, wh), outsideIOR, insideIOR)); float sqrtDenom = Vector3.Dot(outDir, wh) + eta * Vector3.Dot(inDir, wh); float factor = isOnLightSubpath ? (1 / eta) : 1; var numerator = distribution.NormalDistribution(wh) * distribution.MaskingShadowing(outDir, inDir); numerator *= eta * eta * Math.Abs(Vector3.Dot(inDir, wh)) * Math.Abs(Vector3.Dot(outDir, wh)); numerator *= factor * factor; var denom = (cosThetaI * cosThetaO * sqrtDenom * sqrtDenom); Debug.Assert(float.IsFinite(denom)); return((RgbColor.White - F) * transmittance * Math.Abs(numerator / denom)); }
public static void BenchComputeBasisVectors(int numTrials) { Random rng = new(1337); Vector3 NextVector() => new ( (float)rng.NextDouble(), (float)rng.NextDouble(), (float)rng.NextDouble()); Vector3 avg = Vector3.Zero; Stopwatch stop = Stopwatch.StartNew(); for (int i = 0; i < numTrials; ++i) { Vector3 tan, binorm; ShadingSpace.ComputeBasisVectors(NextVector(), out tan, out binorm); avg += (tan + binorm) / numTrials * 0.5f; } Console.WriteLine($"Computing {numTrials} basis vectors took {stop.ElapsedMilliseconds}ms - {avg.Length()}"); }
public (float, float) Pdf(Vector3 outDir, Vector3 inDir, bool isOnLightSubpath) { if (!ShadingSpace.SameHemisphere(outDir, inDir)) { return(0, 0); } var halfVector = outDir + inDir; // catch NaN causing corner cases if (halfVector == Vector3.Zero) { return(0, 0); } halfVector = Vector3.Normalize(halfVector); var pdfForward = distribution.Pdf(outDir, halfVector) / Math.Abs(4 * Vector3.Dot(outDir, halfVector)); var pdfReverse = distribution.Pdf(inDir, halfVector) / Math.Abs(4 * Vector3.Dot(inDir, halfVector)); return(pdfForward, pdfReverse); }
/// <summary> /// Creates a cylinder that connects two given points like a pipe /// </summary> /// <param name="from">The first point, the center of one cylinder disc</param> /// <param name="to">The second point, the center of the other cylinder disc</param> /// <param name="radius">Radius of the cylinder</param> /// <param name="numSegments">Number of quads used to build the outer surface</param> public static Mesh MakeCylinder(Vector3 from, Vector3 to, float radius, int numSegments) { ShadingSpace.ComputeBasisVectors(Vector3.Normalize(to - from), out var tan, out var binorm); List <Vector3> vertices = new(); List <int> indices = new(); for (int i = 0; i < numSegments; ++i) { float angle = i * 2 * MathF.PI / numSegments; float y = MathF.Sin(angle) * radius; float x = MathF.Cos(angle) * radius; vertices.Add(from + tan * x + binorm * y); vertices.Add(to + tan * x + binorm * y); // Indices of the last edge are the first one (close the circle) int nextA = 2 * i + 2; int nextB = 2 * i + 3; if (i == numSegments - 1) { nextA = 0; nextB = 1; } indices.AddRange(new List <int>() { 2 * i, 2 * i + 1, nextA, nextA, 2 * i + 1, nextB }); } Mesh result = new(vertices.ToArray(), indices.ToArray()); return(result); }
public Vector3?Sample(Vector3 outDir, bool isOnLightSubpath, Vector2 primarySample) { if (outDir.Z == 0) { return(null); } var halfVector = distribution.Sample(outDir, primarySample); if (Vector3.Dot(halfVector, outDir) < 0) { return(null); } var inDir = ShadingSpace.Reflect(outDir, halfVector); if (!ShadingSpace.SameHemisphere(outDir, inDir)) { return(null); } return(inDir); }
public override EmitterSample SampleRay(Vector2 primaryPos, Vector2 primaryDir) { var posSample = SampleArea(primaryPos); // Transform primary to cosine hemisphere (z is up) var local = SampleWarp.ToCosHemisphere(primaryDir); // Transform to world space direction var normal = posSample.Point.ShadingNormal; Vector3 tangent, binormal; ShadingSpace.ComputeBasisVectors(normal, out tangent, out binormal); Vector3 dir = local.Direction.Z * normal + local.Direction.X * tangent + local.Direction.Y * binormal; return(new EmitterSample { Point = posSample.Point, Direction = dir, Pdf = local.Pdf * posSample.Pdf, Weight = Radiance / posSample.Pdf * MathF.PI // cosine cancels out with the directional pdf }); }
public RgbColor Evaluate(Vector3 outDir, Vector3 inDir, bool isOnLightSubpath) { if (!ShadingSpace.SameHemisphere(outDir, inDir)) { return(RgbColor.Black); } float cosThetaO = ShadingSpace.AbsCosTheta(outDir); float cosThetaI = ShadingSpace.AbsCosTheta(inDir); Vector3 halfVector = inDir + outDir; // Handle degenerate cases for microfacet reflection if (cosThetaI == 0 || cosThetaO == 0) { return(RgbColor.Black); } if (halfVector.X == 0 && halfVector.Y == 0 && halfVector.Z == 0) { return(RgbColor.Black); } // For the Fresnel call, make sure that wh is in the same hemisphere // as the surface normal, so that total internal reflection is handled correctly. halfVector = Vector3.Normalize(halfVector); if (ShadingSpace.CosTheta(halfVector) < 0) { halfVector = -halfVector; } var cosine = Vector3.Dot(inDir, halfVector); var f = fresnel.Evaluate(cosine); var nd = distribution.NormalDistribution(halfVector); var ms = distribution.MaskingShadowing(outDir, inDir); return(tint * nd * ms * f / (4 * cosThetaI * cosThetaO)); }
static Vector3 TrowbridgeReitzSample(Vector3 wi, float alpha_x, float alpha_y, float U1, float U2) { // 1. stretch wi Vector3 wiStretched = Vector3.Normalize(new Vector3(alpha_x * wi.X, alpha_y * wi.Y, wi.Z)); // 2. simulate P22_{wi}(x_slope, y_slope, 1, 1) float slope_x, slope_y; TrowbridgeReitzSample11(ShadingSpace.CosTheta(wiStretched), U1, U2, out slope_x, out slope_y); // 3. rotate float tmp = ShadingSpace.CosPhi(wiStretched) * slope_x - ShadingSpace.SinPhi(wiStretched) * slope_y; slope_y = ShadingSpace.SinPhi(wiStretched) * slope_x + ShadingSpace.CosPhi(wiStretched) * slope_y; slope_x = tmp; // 4. unstretch slope_x = alpha_x * slope_x; slope_y = alpha_y * slope_y; // 5. compute normal return(Vector3.Normalize(new Vector3(-slope_x, -slope_y, 1))); }
/// <summary> /// The Pdf that is used for importance sampling microfacet normals from this distribution. /// This usually importance samples the portion of normals that are in the hemisphere of the outgoing direction. /// </summary> /// <param name="outDir">The outgoing direction in shading space.</param> /// <param name="inDir">The incoming direction in shading space.</param> /// <returns>The pdf value.</returns> public float Pdf(Vector3 outDir, Vector3 normal) => NormalDistribution(normal) * MaskingShadowing(outDir) * MathF.Abs(Vector3.Dot(outDir, normal)) / ShadingSpace.AbsCosTheta(outDir);