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 }); }
/// <summary> /// Performs the inverse of the projection done by <see cref="Sample"/> /// </summary> /// <param name="point">A point on the surface of this mesh</param> /// <returns>The primary sample space point that would have been projected there</returns> public Vector2 SampleInverse(SurfacePoint point) { var local = SampleWarp.FromUniformTriangle(point.BarycentricCoords); float x = triangleDistribution.SampleInverse((int)point.PrimId, local.X); return(new(x, local.Y)); }
public void FromSphere_ShouldBeInverse() { var primary = new Vector2(0.41f, 0.123f); var dir = SampleWarp.ToUniformSphere(primary).Direction; var p2 = SampleWarp.FromUniformSphere(dir); Assert.Equal(primary.X, p2.X, 4); Assert.Equal(primary.Y, p2.Y, 4); primary = new Vector2(0.91f, 0.00123f); dir = SampleWarp.ToUniformSphere(primary).Direction; p2 = SampleWarp.FromUniformSphere(dir); Assert.Equal(primary.X, p2.X, 4); Assert.Equal(primary.Y, p2.Y, 4); primary = new Vector2(0.091f, 0.00123f); dir = SampleWarp.ToUniformSphere(primary).Direction; p2 = SampleWarp.FromUniformSphere(dir); Assert.Equal(primary.X, p2.X, 4); Assert.Equal(primary.Y, p2.Y, 4); primary = new Vector2(0.91f, 0.823f); dir = SampleWarp.ToUniformSphere(primary).Direction; p2 = SampleWarp.FromUniformSphere(dir); Assert.Equal(primary.X, p2.X, 4); Assert.Equal(primary.Y, p2.Y, 4); }
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); }
public void CosHemisphere_Inverse() { var sample = SampleWarp.ToCosHemisphere(new(0.43f, 0.793f)); var prim = SampleWarp.FromCosHemisphere(sample.Direction); Assert.Equal(0.43f, prim.X, 3); Assert.Equal(0.793f, prim.Y, 3); }
public void Inverse_ShouldBeZero() { var bary = SampleWarp.ToUniformTriangle(new(0.1f, 0.1f)); var prim = SampleWarp.FromUniformTriangle(bary); Assert.Equal(0.1f, prim.X, 4); Assert.Equal(0.1f, prim.Y, 4); }
public void ConcentricDisc_Inverse() { var sample = SampleWarp.ToConcentricDisc(new(0.315f, -0.3154f)); var prim = SampleWarp.FromConcentricDisc(sample); Assert.Equal(0.315f, prim.X, 3); Assert.Equal(-0.3154f, prim.Y, 3); }
public override float RayPdf(Vector3 point, Vector3 direction) { float dirPdf = DirectionPdf(-direction); float discJacobian = SampleWarp.ToConcentricDiscJacobian(); float posPdf = discJacobian / (SceneRadius * SceneRadius); return(posPdf * dirPdf); }
/// <summary> /// Computes the change of area when mapping a direction from the hemisphere around the camera /// to the image. Given by our transformation to spherical coordinates, followed by the scaling to the /// desired resolution. /// </summary> /// <param name="pos">Position in world space of a point towards which the direction points</param> /// <returns> /// Jacobian determinant that describes how much larger an area on the image plane is than /// the corresponding solid angle. /// </returns> public override float SolidAngleToPixelJacobian(Vector3 pos) { var dir = Vector3.Normalize(pos - position); dir = Shading.ShadingSpace.WorldToShading(upVector, dir); float theta = SampleWarp.CartesianToSpherical(dir).Y; return(1 / (2 * MathF.PI * MathF.PI * MathF.Sin(theta)) * width * height); }
Vector3 WorldToFilm(Vector3 pos) { Debug.Assert(width != 0 && height != 0); var dir = pos - position; float distance = dir.Length(); dir = Shading.ShadingSpace.WorldToShading(upVector, dir / distance); var spherical = SampleWarp.CartesianToSpherical(dir); return(new( width * spherical.X / (2 * MathF.PI), height *spherical.Y / MathF.PI, distance )); }
/// <summary> /// Generates a ray from a position in the image into the scene /// </summary> /// <param name="filmPos"> /// Position on the film plane: integer pixel coordinates and fractional position within /// </param> /// <param name="rng"> /// Random number generator used to sample additional decisions (lens position for depth of field) /// </param> /// <returns>The sampled camera ray and related information like PDF and contribution</returns> public override CameraRaySample GenerateRay(Vector2 filmPos, RNG rng) { Debug.Assert(Width != 0 && Height != 0); // Transform the direction from film to world space. // The view space is vertically flipped compared to the film. var view = new Vector3(2 * filmPos.X / Width - 1, 1 - 2 * filmPos.Y / Height, 0); var localDir = Vector3.Transform(view, viewToCamera); var dirHomo = Vector4.Transform(new Vector4(localDir, 0), cameraToWorld); var dir = new Vector3(dirHomo.X, dirHomo.Y, dirHomo.Z); // Compute the camera position var pos = Vector3.Transform(new Vector3(0, 0, 0), cameraToWorld); var ray = new Ray { Direction = Vector3.Normalize(dir), MinDistance = 0, Origin = pos }; // Sample depth of field float pdfLens = 1; if (lensRadius > 0) { var lensSample = rng.NextFloat2D(); var lensPos = lensRadius * SampleWarp.ToConcentricDisc(lensSample); // Intersect ray with focal plane var focalPoint = ray.ComputePoint(focalDistance / ray.Direction.Z); // Update the ray ray.Origin = new Vector3(lensPos, 0); ray.Direction = Vector3.Normalize(focalPoint - ray.Origin); pdfLens = 1 / (MathF.PI * lensRadius * lensRadius); } return(new CameraRaySample { Ray = ray, Weight = RgbColor.White, Point = new SurfacePoint { Position = Position, Normal = Direction }, PdfRay = SolidAngleToPixelJacobian(pos + dir) * pdfLens, PdfConnect = pdfLens }); }
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> /// Samples a point uniformly distributed on the mesh surface /// </summary> /// <param name="primarySample"> /// A primary sample space value that is projected onto the surface of the mesh. /// </param> /// <returns>A point and associated surface area pdf</returns> public SurfaceSample Sample(Vector2 primarySample) { var(faceIdx, newX) = triangleDistribution.Sample(primarySample.X); var barycentric = SampleWarp.ToUniformTriangle(new Vector2(newX, primarySample.Y)); return(new SurfaceSample { Point = new SurfacePoint { BarycentricCoords = barycentric, PrimId = (uint)faceIdx, Normal = FaceNormals[faceIdx], Position = ComputePosition(faceIdx, barycentric), ErrorOffset = ComputeErrorOffset(faceIdx, barycentric), Mesh = this }, Pdf = 1.0f / SurfaceArea }); }
/// <summary> /// Samples a point on the camera lens that sees the given surface point. Returns an invalid /// sample if there is no such point. /// </summary> /// <param name="scenePoint">A point on a scene surface that might be seen by the camera</param> /// <param name="rng">RNG used to sample the lens. Can be null if the lens radius is zero.</param> /// <returns>The pixel coordinates and weights, or an invalid sample</returns> public override CameraResponseSample SampleResponse(SurfacePoint scenePoint, RNG rng) { Debug.Assert(Width != 0 && Height != 0); // Sample a point on the lens Vector3 lensPoint = Position; if (lensRadius > 0) { var lensSample = rng.NextFloat2D(); var lensPosCam = lensRadius * SampleWarp.ToConcentricDisc(lensSample); var lensPosWorld = Vector4.Transform(new Vector4(lensPosCam.X, lensPosCam.Y, 0, 1), cameraToWorld); lensPoint = new(lensPosWorld.X, lensPosWorld.Y, lensPosWorld.Z); } // Map the scene point to the film var filmPos = WorldToFilm(scenePoint.Position); if (!filmPos.HasValue) { return(CameraResponseSample.Invalid); } // Compute the change of variables from scene surface to pixel area float jacobian = SolidAngleToPixelJacobian(scenePoint.Position); jacobian *= SurfaceAreaToSolidAngleJacobian(scenePoint.Position, scenePoint.Normal); // Compute the pdfs float invLensArea = 1; if (lensRadius > 0) { invLensArea = 1 / (MathF.PI * lensRadius * lensRadius); } float pdfConnect = invLensArea; float pdfEmit = invLensArea * jacobian; return(new CameraResponseSample { Pixel = new(filmPos.Value.X, filmPos.Value.Y), Position = lensPoint, Weight = jacobian * RgbColor.White, PdfConnect = pdfConnect, PdfEmit = pdfEmit });
public void SphericalInverse() { // Y axis var spherical = SampleWarp.CartesianToSpherical(Vector3.UnitY); var dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(0, dir.X, 4); Assert.Equal(1, dir.Y, 4); Assert.Equal(0, dir.Z, 4); spherical = SampleWarp.CartesianToSpherical(-Vector3.UnitY); dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(0, dir.X, 4); Assert.Equal(-1, dir.Y, 4); Assert.Equal(0, dir.Z, 4); // x axis spherical = SampleWarp.CartesianToSpherical(Vector3.UnitX); dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(1, dir.X, 4); Assert.Equal(0, dir.Y, 4); Assert.Equal(0, dir.Z, 4); spherical = SampleWarp.CartesianToSpherical(-Vector3.UnitX); dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(-1, dir.X, 4); Assert.Equal(0, dir.Y, 4); Assert.Equal(0, dir.Z, 4); // z axis spherical = SampleWarp.CartesianToSpherical(Vector3.UnitZ); dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(0, dir.X, 4); Assert.Equal(0, dir.Y, 4); Assert.Equal(1, dir.Z, 4); spherical = SampleWarp.CartesianToSpherical(-Vector3.UnitZ); dir = SampleWarp.SphericalToCartesian(spherical); Assert.Equal(0, dir.X, 4); Assert.Equal(0, dir.Y, 4); Assert.Equal(-1, dir.Z, 4); }
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 override float PdfRay(SurfacePoint point, Vector3 direction) { float cosine = Vector3.Dot(point.ShadingNormal, direction) / direction.Length(); return(PdfArea(point) * SampleWarp.ToCosineLobeJacobian(exponent + 1, cosine)); }
RgbColor ContinueWalk(Ray ray, SurfacePoint previousPoint, float pdfDirection, RgbColor throughput, int depth) { // Terminate if the maximum depth has been reached if (depth >= maxDepth) { OnTerminate(); return(RgbColor.Black); } var hit = scene.Raytracer.Trace(ray); if (!hit) { var result = OnInvalidHit(ray, pdfDirection, throughput, depth); OnTerminate(); return(result); } // Convert the PDF of the previous hemispherical sample to surface area float pdfFromAncestor = pdfDirection * SampleWarp.SurfaceAreaToSolidAngle(previousPoint, hit); // Geometry term might be zero due to, e.g., shading normal issues // Avoid NaNs in that case by terminating early if (pdfFromAncestor == 0) { OnTerminate(); return(RgbColor.Black); } RgbColor estimate = OnHit(ray, hit, pdfFromAncestor, throughput, depth, SampleWarp.SurfaceAreaToSolidAngle(hit, previousPoint)); // Terminate with Russian roulette float survivalProb = ComputeSurvivalProbability(hit, ray, throughput, depth); if (rng.NextFloat() > survivalProb) { OnTerminate(); return(estimate); } // Continue based on the splitting factor int numSplits = ComputeSplitFactor(hit, ray, throughput, depth); for (int i = 0; i < numSplits; ++i) { // Sample the next direction and convert the reverse pdf var(pdfNext, pdfReverse, weight, direction) = SampleNextDirection(hit, ray, throughput, depth); float pdfToAncestor = pdfReverse * SampleWarp.SurfaceAreaToSolidAngle(hit, previousPoint); if (pdfToAncestor == 0) { Debug.Assert(pdfNext == 0); } OnContinue(pdfToAncestor, depth); if (pdfNext == 0 || weight == RgbColor.Black) { OnTerminate(); continue; } // Account for splitting and roulette in the weight weight *= 1.0f / (survivalProb * numSplits); // Continue the path with the next ray var nextRay = Raytracer.SpawnRay(hit, direction); estimate += ContinueWalk(nextRay, hit, pdfNext, throughput * weight, depth + 1); } return(estimate); }