/// <summary> /// Calculates ambient occlusion at a point on a surface. /// </summary> /// <param name="surface">Information about surface point</param> /// <param name="geometry">The geometry to be raytraced (both occlusion caster and occlusion receiver)</param> /// <param name="random">Random number generator to use</param> /// <returns>Fraction of non-occlusion at surface point (between 0 and 1): 0 = surface point fully occluded by neighbour surfaces; 1 = surface point not occluded at all</returns> private double CalcAmbientOcclusion(IntersectionInfo surface, Raytrace.IRayIntersectable geometry, RenderContext context) { Contract.Ensures(0 <= Contract.Result <double>() && Contract.Result <double>() <= 1); var random = context.RNG; var rayStart = surface.pos + surface.normal * ambientOcclusionProbeOffset; Vector avgEscapedRayDir = new Vector(); int rayEscapeCount = 0; for (int i = 0; i < ambientOcclusionQuality; i++) { // Pick a random direction within the hemisphere around the surface normal // TODO: works for external surfaces, but some self-intersection on interior surfaces var rayDir = new Vector(random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1); //rayDir.Normalise(); if (rayDir.DotProduct(surface.normal) < 0) { rayDir = -rayDir; } // Pick random directions until we find one roughly in the same direction as the surface normal //Vector rayDir; //double cosOfAngle; //do //{ // rayDir = new Vector(random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1, random.NextDouble() * 2 - 1); // rayDir.Normalise(); // cosOfAngle = rayDir.DotProduct(surfaceInfo.normal); // // force ray to be in the hemisphere around the surface normal //} while (cosOfAngle < 0.0); // Fire off ray to check for nearby surface in chosen direction // TODO: might be more efficient if ray tracing stopped after a short distance from ray origin Raytrace.IntersectionInfo shadowInfo = geometry.IntersectRay(rayStart, rayDir, context); if (shadowInfo == null || shadowInfo.rayFrac > ambientOcclusionProbeDist) { // This ray did not hit a nearby surface rayEscapeCount++; avgEscapedRayDir += rayDir; } } // visualise direction of unobstructed space around surface point //avgEscapedRayDir.Normalise(); //var v = new Vector(avgEscapedRayDir.x + 1, avgEscapedRayDir.y + 1, avgEscapedRayDir.z + 1); //v *= 128; //return Surface.PackRgb((byte)v.x, (byte)v.y, (byte)v.z); //avgEscapedRayDir.Normalise(); //avgEscapedRayDir *= 255; //return Surface.PackRgb((byte)avgEscapedRayDir.x, (byte)avgEscapedRayDir.y, (byte)avgEscapedRayDir.z); return((double)rayEscapeCount / (double)ambientOcclusionQuality); }
//public void CalcAllAmbientOcclusion() //{ // // TODO: write brute force algorithm: for every cell in 3D cube, find intersecting triangles and calc AO for these? Or just find nearest triangle to each cell center? //} /// <summary> /// Calculates and caches ambient occlusion at a point on a surface. /// Multithread safe. /// </summary> /// <param name="surface">Information about surface point</param> /// <param name="geometry">The geometry to be raytraced (both occlusion caster and occlusion receiver)</param> /// <param name="random">Random number generator to use. Set to <value>NULL</value> to use the default random number generator</param> /// <returns>Fraction of non-occlusion at surface point (between 1 and 255): 1 = surface point fully occluded by neighbour surfaces; 255 = surface point not occluded at all</returns> public T CacheAmbientOcclusion(IntersectionInfo surface, Raytrace.IRayIntersectable geometry, RenderContext context) { Contract.Requires(surface != null); // TODO: Sometimes a surface point coordinate is very slightly outside the unit cube (i.e. Z coordinate of 0.50000000001) const double maxExtent = 0.5; // TODO: extent seems too large on Couch model... const double scale = 0.5 / maxExtent; // So we clamp coordinates to the unit cube surface.pos.x = Math.Min(Math.Max(-0.5, surface.pos.x), 0.5); surface.pos.y = Math.Min(Math.Max(-0.5, surface.pos.y), 0.5); surface.pos.z = Math.Min(Math.Max(-0.5, surface.pos.z), 0.5); /* * Assert.IsTrue(-maxExtent <= surface.pos.x && surface.pos.x <= maxExtent, "Surface point is outside unit cube"); * Assert.IsTrue(-maxExtent <= surface.pos.y && surface.pos.y <= maxExtent, "Surface point is outside unit cube"); * Assert.IsTrue(-maxExtent <= surface.pos.z && surface.pos.z <= maxExtent, "Surface point is outside unit cube"); */ var cacheIndex = (int)((surface.pos.x * scale + 0.5) * (cacheSize - 1)) * cacheSize * cacheSize + (int)((surface.pos.y * scale + 0.5) * (cacheSize - 1)) * cacheSize + (int)((surface.pos.z * scale + 0.5) * (cacheSize - 1)); // fetch cache entry, but if missing then calc AO factor // TODO: probably not multithread safe! // TODO: tri-linearly interpolate AO factor from eight neighbouring cache entries for smoother results! // Cheap thread lock - prevent other threads recalculating this value, while it is calculated by this thread // TODO: cannot be used with byte values, but could be used with ints //if(0 == Interlocked.CompareExchange<byte>(ref cacheData[cacheIndex], (byte)1, (byte)0)) if (!EnableCache) { // calc shading intensity based on percentage AO shadowing double unoccludedFactor = CalcAmbientOcclusion(surface, geometry, context); // scale to range [1, 255]. We avoid zero as this is a sentinel value indicating empty cache entry. return((T)(unoccludedFactor * 254 + 1)); } // Double-check locking pattern // TODO: rare IndexOutOfRangeException here T lightByte = cacheData[cacheIndex]; if (EmptyCacheEntry == lightByte) { lock (calcLock) { // another thread may have got the lock first, and already calculated this value if (EmptyCacheEntry == cacheData[cacheIndex]) { // calc shading intensity based on percentage AO shadowing double unoccludedFactor = CalcAmbientOcclusion(surface, geometry, context); // scale to range [1, 255]. We avoid zero as this is a sentinel value indicating empty cache entry. lightByte = (T)(unoccludedFactor * 254 + 1); cacheData[cacheIndex] = lightByte; // Time to persist cache data to file in isolated storage? numCalcsUntilNextPersist--; if (numCalcsUntilNextPersist <= 0) { // TODO: lock may be held for long time! SaveCacheToDisk(cacheFilePath, cacheData); numCalcsUntilNextPersist = numCalcsBetweenPersists; } } } } return(lightByte); }