public TerrainNode(Terrain terrain, Material material) { if (terrain == null) { throw new ArgumentNullException("terrain"); } if (material == null) { throw new ArgumentNullException("material"); } Terrain = terrain; Material = material; IsRenderable = true; CastsShadows = true; Shape = terrain.Shape; _baseClipmap = new TerrainClipmap(1, SurfaceFormat.HalfVector4) { LevelBias = 0.1f, }; _detailClipmap = new TerrainClipmap(3, SurfaceFormat.Color) { CellsPerLevel = 1024, NumberOfLevels = 6, }; DetailClipmap.CellSizes[0] = 0.005f; DetailClipmap.Invalidate(); HoleThreshold = 0.3f; DetailFadeRange = 0.3f; }
//private static Vector2F RoundDownToGrid(Vector2F position, float cellSize) //{ // position.X = RoundDownToGrid(position.X, cellSize); // position.Y = RoundDownToGrid(position.Y, cellSize); // return position; //} //private static Vector2F RoundUpToGrid(Vector2F position, float cellSize) //{ // position.X = RoundUpToGrid(position.X, cellSize); // position.Y = RoundUpToGrid(position.Y, cellSize); // return position; //} ///// <summary> ///// Converts a world space position to texture coordinates for a given clipmap level. ///// (Does not handle texture atlas.) ///// </summary> ///// <param name="position">The world space xz position.</param> ///// <param name="levelOrigin">The world space xz level origin.</param> ///// <param name="levelSize">The world space size of the level.</param> ///// <returns> ///// The texture coordinates. ///// </returns> //private static Vector2F ConvertPositionToTexCoord(Vector2F position, Vector2F levelOrigin, float levelSize) //{ // return (position - levelOrigin) / levelSize; //} //private static Vector2F ConvertTexCoordToClipmapTextureAtlas(Vector2F texCoord, int level, int numberOfLevels, int numberOfColumns) //{ // // The clipmaps are stored in a texture atlas. // Debug.Assert(texCoord.X >= -0.00001); // Debug.Assert(texCoord.Y >= -0.00001); // Debug.Assert(texCoord.X <= 1.0001); // Debug.Assert(texCoord.Y <= 1.0001); // int numberOfRows = (numberOfLevels - 1) / numberOfColumns + 1; // int column = level % numberOfColumns; // int row = level / numberOfColumns; // texCoord.X = (texCoord.X + column) / numberOfColumns; // texCoord.Y = (texCoord.Y + row) / numberOfRows; // return texCoord; //} //private static Rectangle GetScreenSpaceRectangle(TerrainClipmap clipmap, int level, float levelSize, Vector2F startPosition, Vector2F endPosition, Viewport viewport) //{ // int numberOfLevels = clipmap.NumberOfLevels; // int numberOfColumns = clipmap.NumberOfTextureAtlasColumns; // var levelOrigin = clipmap.Origins[level]; // // TexCoords relative to a non-texture atlas texture. // var startTexCoord = ConvertPositionToTexCoord(startPosition, levelOrigin, levelSize); // var endTexCoord = ConvertPositionToTexCoord(endPosition, levelOrigin, levelSize); // // TexCoords relative to the texture atlas. // startTexCoord = ConvertTexCoordToClipmapTextureAtlas(startTexCoord, level, clipmap.NumberOfLevels, numberOfColumns); // endTexCoord = ConvertTexCoordToClipmapTextureAtlas(endTexCoord, level, numberOfLevels, numberOfColumns); // Debug.Assert(Numeric.AreEqual((int)(startTexCoord.X * viewport.Width + 0.5), startTexCoord.X * viewport.Width), // "Clipmap coordinates are not snapped to grid positions."); // return new Rectangle( // (int)(startTexCoord.X * viewport.Width + 0.5), // (int)(startTexCoord.Y * viewport.Height + 0.5), // (int)((endTexCoord.X - startTexCoord.X) * viewport.Width + 0.5), // (int)((endTexCoord.Y - startTexCoord.Y) * viewport.Height + 0.5)); //} /// <summary> /// Gets the screen space rectangle of the given clipmap level. /// </summary> private static Rectangle GetScreenSpaceRectangle(TerrainClipmap clipmap, int level) { //int numberOfLevels = clipmap.NumberOfLevels; int numberOfColumns = clipmap.NumberOfTextureAtlasColumns; //int numberOfRows = (numberOfLevels - 1) / numberOfColumns + 1; int column = level % numberOfColumns; int row = level / numberOfColumns; int cellsPerLevel = clipmap.CellsPerLevel; return new Rectangle(column * cellsPerLevel, row * cellsPerLevel, cellsPerLevel, cellsPerLevel); }
/// <summary> /// Initializes TerrainClipmap.Textures (and also updates TerrainClipmap.UseIncrementalUpdate). /// </summary> /// <param name="graphicsDevice">The graphics device.</param> /// <param name="clipmap">The clipmap.</param> private static void InitializeClipmapTextures(GraphicsDevice graphicsDevice, TerrainClipmap clipmap) { int width, height; GetClipmapSize(clipmap.NumberOfLevels, clipmap.CellsPerLevel, out width, out height); for (int i = 0; i < clipmap.Textures.Length; i++) { if (clipmap.Textures[i] == null || clipmap.Textures[i].IsDisposed || clipmap.Textures[i].Width != width || clipmap.Textures[i].Height != height // If width or height > 1 then check for mipmaps. || (width > 1 || height > 1) && (clipmap.Textures[i].LevelCount > 1) != clipmap.EnableMipMap ) { // Texture format has changed. clipmap.UseIncrementalUpdate = false; clipmap.Textures[i].SafeDispose(); try { bool enableMips = clipmap.EnableMipMap; clipmap.Textures[i] = new RenderTarget2D( graphicsDevice, width, height, enableMips, clipmap.Format, DepthFormat.None, 0, RenderTargetUsage.PreserveContents); } catch (Exception exception) { throw new GraphicsException( "Could not create terrain clipmap. See inner exception for details.", exception); } } else { // Texture format unchanged. if (((RenderTarget2D)clipmap.Textures[i]).IsContentLost) { clipmap.UseIncrementalUpdate = false; } } } }
public TerrainNode(Terrain terrain, Material material) { if (terrain == null) throw new ArgumentNullException("terrain"); if (material == null) throw new ArgumentNullException("material"); Terrain = terrain; Material = material; IsRenderable = true; CastsShadows = true; Shape = terrain.Shape; _baseClipmap = new TerrainClipmap(1, SurfaceFormat.HalfVector4) { LevelBias = 0.1f, }; _detailClipmap = new TerrainClipmap(3, SurfaceFormat.Color) { CellsPerLevel = 1024, NumberOfLevels = 6, }; DetailClipmap.CellSizes[0] = 0.005f; DetailClipmap.Invalidate(); HoleThreshold = 0.3f; DetailFadeRange = 0.3f; }
private void ProcessLayer(GraphicsDevice graphicsDevice, RenderContext context, TerrainClipmap clipmap, bool isBaseClipmap, IInternalTerrainLayer layer, Aabb tileAabb) { // The material needs to have a "Base" or "Detail" render pass. if (!layer.Material.Contains(context.RenderPass)) { return; } var layerAabb = layer.Aabb ?? tileAabb; //if (!HaveContactXZ(ref layerAabb, ref clipmap.Aabb)) // continue; bool isInInvalidRegion = false; for (int level = clipmap.NumberOfLevels - 1; level >= 0; level--) { for (int i = 0; i < clipmap.InvalidRegions[level].Count; i++) { var aabb = clipmap.InvalidRegions[level][i]; if (HaveContactXZ(ref layerAabb, ref aabb)) { isInInvalidRegion = true; break; } } if (isInInvalidRegion) { break; } } if (!isInInvalidRegion) { return; } // Reset render state for each layer because layers are allowed to change render state // without restoring it in a restore pass. // The base clipmap uses no blending. The detail clipmap uses alpha-blending (but only in // RGB, A is not changed, A is used e.g. for hole info). graphicsDevice.BlendState = isBaseClipmap ? BlendState.Opaque : BlendStateAlphaBlendRgb; graphicsDevice.RasterizerState = RasterizerStateCullNoneWithScissorTest; graphicsDevice.DepthStencilState = DepthStencilState.None; // Get the EffectBindings and the Effect for the current render pass. EffectBinding materialInstanceBinding = layer.MaterialInstance[context.RenderPass]; EffectBinding materialBinding = layer.Material[context.RenderPass]; EffectEx effectEx = materialBinding.EffectEx; Effect effect = materialBinding.Effect; context.MaterialInstanceBinding = materialInstanceBinding; context.MaterialBinding = materialBinding; if (effectEx.Id == 0) { effectEx.Id = 1; // Update and apply global effect parameter bindings - these bindings set the // effect parameter values for "global" parameters. For example, if an effect uses // a "ViewProjection" parameter, then a binding will compute this matrix from the // current CameraInstance in the render context and update the effect parameter. foreach (var binding in effect.GetParameterBindings()) { if (binding.Description.Hint == EffectParameterHint.Global) { binding.Update(context); binding.Apply(context); } } } // Update and apply material bindings. // If this material is the same as in the last ProcessLayer() call, then we can skip this. if (_previousMaterialBinding != materialBinding) { _previousMaterialBinding = materialBinding; if (materialBinding.Id == 0) { materialBinding.Id = 1; foreach (var binding in materialBinding.ParameterBindings) { binding.Update(context); binding.Apply(context); } } else { // The material has already been updated in this frame. foreach (var binding in materialBinding.ParameterBindings) { binding.Apply(context); } } } // Update and apply local, per-instance, and per-pass bindings - these are bindings // for parameters, like the "World" matrix or lighting parameters. foreach (var binding in materialInstanceBinding.ParameterBindings) { if (binding.Description.Hint != EffectParameterHint.PerPass) { binding.Update(context); binding.Apply(context); } } // Select and apply technique. var techniqueBinding = materialInstanceBinding.TechniqueBinding; techniqueBinding.Update(context); var technique = techniqueBinding.GetTechnique(effect, context); effect.CurrentTechnique = technique; int border = isBaseClipmap ? 0 : Border; // Region covered by the layer. Vector2F layerStart = new Vector2F(layerAabb.Minimum.X, layerAabb.Minimum.Z); Vector2F layerEnd = new Vector2F(layerAabb.Maximum.X, layerAabb.Maximum.Z); // Clamp to tile AABB layerStart.X = Math.Max(layerStart.X, tileAabb.Minimum.X); layerStart.Y = Math.Max(layerStart.Y, tileAabb.Minimum.Z); layerEnd.X = Math.Min(layerEnd.X, tileAabb.Maximum.X); layerEnd.Y = Math.Min(layerEnd.Y, tileAabb.Maximum.Z); // Loop over clipmap levels. for (int level = 0; level < clipmap.NumberOfLevels; level++) { // If there are no invalid regions, the combined AABB is NaN. if (Numeric.IsNaN(clipmap.CombinedInvalidRegionsAabbs[level].Minimum.X)) { continue; } if (layer.FadeInStart > level || layer.FadeOutEnd <= level) { continue; } if (!HaveContactXZ(ref layerAabb, ref clipmap.CombinedInvalidRegionsAabbs[level])) { continue; } Vector2F levelOrigin = clipmap.Origins[level]; Vector2F levelSize = new Vector2F(clipmap.LevelSizes[level]); // without border! Vector2F levelEnd = levelOrigin + levelSize; float cellsPerLevelWithoutBorder = clipmap.CellsPerLevel - 2 * border; float cellSize = clipmap.ActualCellSizes[level]; float texelsPerUnit = 1 / cellSize; // Following effect parameters change per clipmap level: UpdateAndApplyParameter(materialBinding, "TerrainClipmapLevel", (float)level, context); UpdateAndApplyParameter(materialBinding, "TerrainClipmapCellSize", cellSize, context); // The rectangle of the whole clipmap level (including border). var levelRect = GetScreenSpaceRectangle(clipmap, level); // The pixel position of the offset. int offsetX = levelRect.X + border + (int)(cellsPerLevelWithoutBorder * clipmap.Offsets[level].X + 0.5f); int offsetY = levelRect.Y + border + (int)(cellsPerLevelWithoutBorder * clipmap.Offsets[level].Y + 0.5f); // Handle the 4 rectangles of the toroidally wrapped clipmap. bool applyPass = true; for (int i = 0; i < 4; i++) { Rectangle quadrantRect; Vector2F offsetPosition; switch (i) { case 0: // Top left rectangle. quadrantRect = new Rectangle(levelRect.X, levelRect.Y, offsetX - levelRect.X, offsetY - levelRect.Y); offsetPosition = levelEnd; break; case 1: // Top right rectangle. quadrantRect = new Rectangle(offsetX, levelRect.Y, levelRect.Right - offsetX, offsetY - levelRect.Y); offsetPosition.X = levelOrigin.X; offsetPosition.Y = levelEnd.Y; break; case 2: // Bottom left rectangle. quadrantRect = new Rectangle(levelRect.X, offsetY, offsetX - levelRect.X, levelRect.Bottom - offsetY); offsetPosition.X = levelEnd.X; offsetPosition.Y = levelOrigin.Y; break; default: // Bottom right rectangle. quadrantRect = new Rectangle(offsetX, offsetY, levelRect.Right - offsetX, levelRect.Bottom - offsetY); offsetPosition = levelOrigin; break; } if (quadrantRect.Width == 0 || quadrantRect.Height == 0) { continue; } applyPass |= UpdateAndApplyParameter(materialBinding, "TerrainClipmapOffsetWorld", (Vector2)offsetPosition, context); applyPass |= UpdateAndApplyParameter(materialBinding, "TerrainClipmapOffsetScreen", new Vector2(offsetX, offsetY), context); var passBinding = techniqueBinding.GetPassBinding(technique, context); foreach (var pass in passBinding) { // Update and apply per-pass bindings. foreach (var binding in materialInstanceBinding.ParameterBindings) { if (binding.Description.Hint == EffectParameterHint.PerPass) { binding.Update(context); binding.Apply(context); applyPass = true; } } if (applyPass) { pass.Apply(); applyPass = false; } foreach (var aabb in clipmap.InvalidRegions[level]) { // Intersect layer AABB with invalid region AABB. Vector2F clippedLayerStart, clippedLayerEnd; clippedLayerStart.X = Math.Max(layerStart.X, aabb.Minimum.X); clippedLayerStart.Y = Math.Max(layerStart.Y, aabb.Minimum.Z); clippedLayerEnd.X = Math.Min(layerEnd.X, aabb.Maximum.X); clippedLayerEnd.Y = Math.Min(layerEnd.Y, aabb.Maximum.Z); // Nothing to do if layer AABB does not intersect invalid region. if (clippedLayerStart.X >= clippedLayerEnd.X || clippedLayerStart.Y >= clippedLayerEnd.Y) { continue; } // Compute screen space rectangle of intersection (relative to toroidal offset). var invalidRect = GetScissorRectangle( clippedLayerStart - offsetPosition, clippedLayerEnd - clippedLayerStart, texelsPerUnit); // Add toroidal offset screen position. invalidRect.X += offsetX; invalidRect.Y += offsetY; // Set a scissor rectangle to avoid drawing outside the current toroidal wrap // part and outside the invalid region. var scissorRect = Rectangle.Intersect(quadrantRect, invalidRect); if (scissorRect.Width <= 0 || scissorRect.Height <= 0) { continue; } graphicsDevice.ScissorRectangle = scissorRect; // Compute world space position of scissor rectangle corners. Vector2F start, end; start.X = offsetPosition.X + (scissorRect.X - offsetX) * cellSize; start.Y = offsetPosition.Y + (scissorRect.Y - offsetY) * cellSize; end.X = offsetPosition.X + (scissorRect.Right - offsetX) * cellSize; end.Y = offsetPosition.Y + (scissorRect.Bottom - offsetY) * cellSize; Debug.Assert(Numeric.IsLessOrEqual(start.X, end.X)); Debug.Assert(Numeric.IsLessOrEqual(start.Y, end.Y)); layer.OnDraw(graphicsDevice, scissorRect, start, end); } } } } }
private void ComputeInvalidRegions(TerrainNode node, TerrainClipmap clipmap, bool isBaseClipmap) { // Note: // This method computes the AABBs of terrain parts that need to be updated. // Important: AABBs must not overlap because the materials can use alpha blending and they // must not blend twice into the same area. var terrain = node.Terrain; var invalidRegions = isBaseClipmap ? terrain.InvalidBaseRegions : terrain.InvalidDetailRegions; var areInvalidRegionsClipped = isBaseClipmap ? terrain.AreInvalidBaseRegionsClipped : terrain.AreInvalidDetailRegionsClipped; float border = isBaseClipmap ? 0 : Border; for (int level = 0; level < clipmap.NumberOfLevels; level++) { if (clipmap.InvalidRegions[level] == null) { clipmap.InvalidRegions[level] = new List <Aabb>(); } // Compute AABB of whole level. float borderWorld = border * clipmap.ActualCellSizes[level]; Vector2F newOrigin = clipmap.Origins[level]; var aabb = new Aabb { Minimum = { X = newOrigin.X - borderWorld, Y = float.MinValue, Z = newOrigin.Y - borderWorld }, Maximum = { X = newOrigin.X + clipmap.LevelSizes[level] + borderWorld, Y = float.MaxValue, Z = newOrigin.Y + clipmap.LevelSizes[level] + borderWorld } }; // Store AABB of whole clipmap. if (level == clipmap.NumberOfLevels - 1) { clipmap.Aabb = aabb; } // For debugging: //var oldOrigin = clipmap.OldOrigins[level]; //if (oldOrigin != newOrigin) // clipmap.InvalidRegions[level].Add(aabb); if (!clipmap.UseIncrementalUpdate) { // Incremental update not possible. --> The whole clipmap is the invalid region. clipmap.InvalidRegions[level].Add(aabb); } else { // The clipmap contains info from the last frame and the layers where not changed. // We can use a toroidal update. Vector2F oldOrigin = clipmap.OldOrigins[level]; float levelSize = clipmap.LevelSizes[level]; if (oldOrigin != newOrigin) { // The camera or the clipmap level origin has moved. if (oldOrigin.X >= newOrigin.X + levelSize || oldOrigin.Y >= newOrigin.Y + levelSize || newOrigin.X >= oldOrigin.X + levelSize || newOrigin.Y >= oldOrigin.Y + levelSize || isBaseClipmap) // Base clipmap does not (yet) use toroidal wrap. { // We moved a lot and we cannot reuse anything from the last frame. clipmap.InvalidRegions[level].Clear(); clipmap.InvalidRegions[level].Add(aabb); clipmap.Offsets[level] = new Vector2F(0); } else { // The origin has moved. An L shape has to be updated. --> We need two AABBs. // To avoid an unnecessary overlap: The horizontal AABB covers the whole width. // The vertical AABB is shorter to avoid overlap with the horizontal AABB. Aabb horizontalAabb = new Aabb(); horizontalAabb.Minimum.X = newOrigin.X; horizontalAabb.Maximum.X = newOrigin.X + levelSize; horizontalAabb.Minimum.Y = float.MinValue; horizontalAabb.Maximum.Y = float.MaxValue; if (oldOrigin.Y <= newOrigin.Y) { // Origin moved down. horizontalAabb.Minimum.Z = oldOrigin.Y + levelSize; horizontalAabb.Maximum.Z = newOrigin.Y + levelSize; } else { // Origin moved up. horizontalAabb.Minimum.Z = newOrigin.Y; horizontalAabb.Maximum.Z = oldOrigin.Y; } Aabb verticalAabb = new Aabb(); verticalAabb.Minimum.Y = float.MinValue; verticalAabb.Maximum.Y = float.MaxValue; verticalAabb.Minimum.Z = newOrigin.Y; verticalAabb.Maximum.Z = newOrigin.Y + levelSize; if (oldOrigin.X <= newOrigin.X) { // Origin moved right. verticalAabb.Minimum.X = oldOrigin.X + levelSize; verticalAabb.Maximum.X = newOrigin.X + levelSize; } else { // Origin moved left. verticalAabb.Minimum.X = newOrigin.X; verticalAabb.Maximum.X = oldOrigin.X; } Debug.Assert(horizontalAabb.Minimum <= horizontalAabb.Maximum); Debug.Assert(verticalAabb.Minimum <= verticalAabb.Maximum); Debug.Assert(Numeric.AreEqual(horizontalAabb.Extent.X, levelSize)); // (Assertions need larger epsilon.) //Debug.Assert(Numeric.AreEqual(horizontalAabb.Extent.Z, Math.Abs(oldOrigin.Y - newOrigin.Y))); //Debug.Assert(Numeric.AreEqual(verticalAabb.Extent.X, Math.Abs(oldOrigin.X - newOrigin.X))); // Add the border for texture filtering. horizontalAabb.Minimum.X -= borderWorld; horizontalAabb.Minimum.Z -= borderWorld; horizontalAabb.Maximum.X += borderWorld; horizontalAabb.Maximum.Z += borderWorld; verticalAabb.Minimum.X -= borderWorld; verticalAabb.Minimum.Z -= borderWorld; verticalAabb.Maximum.X += borderWorld; verticalAabb.Maximum.Z += borderWorld; // AABBs must not overlap. if (oldOrigin.Y <= newOrigin.Y) { verticalAabb.Maximum.Z = horizontalAabb.Minimum.Z; } else { verticalAabb.Minimum.Z = horizontalAabb.Maximum.Z; } Debug.Assert(horizontalAabb.Minimum.X >= verticalAabb.Maximum.X || horizontalAabb.Maximum.X <= verticalAabb.Minimum.X || horizontalAabb.Minimum.Y >= verticalAabb.Maximum.Y || horizontalAabb.Maximum.Y <= verticalAabb.Minimum.Y || horizontalAabb.Minimum.Z >= verticalAabb.Maximum.Z || horizontalAabb.Maximum.Z <= verticalAabb.Minimum.Z, "Invalid region AABBs must not overlap."); if (clipmap.InvalidRegions[level].Count == 0) { clipmap.InvalidRegions[level].Add(horizontalAabb); clipmap.InvalidRegions[level].Add(verticalAabb); } else { AddAabbWithClipping(clipmap.InvalidRegions[level], ref horizontalAabb); AddAabbWithClipping(clipmap.InvalidRegions[level], ref verticalAabb); } } } if (!areInvalidRegionsClipped && invalidRegions.Count > 1) { // Clip the invalid regions stored in the Terrain once. _aabbList.Clear(); for (int i = 0; i < invalidRegions.Count; i++) { var invalidAabb = invalidRegions[i]; AddAabbWithClipping(_aabbList, ref invalidAabb); } invalidRegions.Clear(); invalidRegions.AddRange(_aabbList); // Update flag (also in terrain in case other TerrainNodes use the same Terrain). areInvalidRegionsClipped = true; if (isBaseClipmap) { terrain.AreInvalidBaseRegionsClipped = true; } else { terrain.AreInvalidDetailRegionsClipped = true; } } if (invalidRegions.Count > 0) { // Add all invalid regions - without overlap. for (int i = 0; i < invalidRegions.Count; i++) { var invalidAabb = invalidRegions[i]; AddAabbWithClipping(clipmap.InvalidRegions[level], ref invalidAabb); } } } // Assert: Invalid regions do not overlap. for (int i = 0; i < clipmap.InvalidRegions[level].Count; i++) { var a = clipmap.InvalidRegions[level][i]; for (int j = i + 1; j < clipmap.InvalidRegions[level].Count; j++) { var b = clipmap.InvalidRegions[level][j]; Debug.Assert(!HaveContactXZ(ref a, ref b)); } } // Compute the union of all invalid regions. We can use this to early out if a layer // does not overlap this combined region. var numberOfInvalidRegions = clipmap.InvalidRegions[level].Count; if (numberOfInvalidRegions == 0) { clipmap.CombinedInvalidRegionsAabbs[level] = new Aabb(new Vector3(float.NaN), new Vector3(float.NaN)); } else { clipmap.CombinedInvalidRegionsAabbs[level] = clipmap.InvalidRegions[level][0]; for (int i = 1; i < numberOfInvalidRegions; i++) { clipmap.CombinedInvalidRegionsAabbs[level].Grow(clipmap.InvalidRegions[level][i]); } } // For debugging: //clipmap.CombinedInvalidRegionsAabbs[level] = new Aabb(new Vector3(float.MinValue), new Vector3(float.MaxValue)); } }
private static void UpdateClipmapData(TerrainNode node, TerrainClipmap clipmap, CameraNode lodCameraNode, bool isBaseClipmap) { int border = isBaseClipmap ? 0 : Border; var terrain = node.Terrain; var terrainAabb = terrain.Aabb; Vector2F terrainAabbMin = new Vector2F(terrainAabb.Minimum.X, terrainAabb.Minimum.Z); Vector2F terrainAabbMax = new Vector2F(terrainAabb.Maximum.X, terrainAabb.Maximum.Z); for (int level = clipmap.NumberOfLevels - 1; level >= 0; level--) { // Compute new origins. int texelsPerLevel = clipmap.CellsPerLevel - 2 * border; Vector3 referencePosition3D = lodCameraNode.PoseWorld.Position; Vector2F referencePosition2D = new Vector2F(referencePosition3D.X, referencePosition3D.Z); clipmap.LevelSizes[level] = clipmap.ActualCellSizes[level] * texelsPerLevel; Vector2F levelOrigin = new Vector2F( referencePosition2D.X - clipmap.LevelSizes[level] / 2, referencePosition2D.Y - clipmap.LevelSizes[level] / 2); // Do not move detail clipmap outside the terrain. This would only waste resources. if (!isBaseClipmap) { // The fade range is hardcoded to 15% of the radius in Terrain.fxh. float fadeRange = clipmap.LevelSizes[level] * node.DetailFadeRange / 2; // / 2 because its relative to the radius. // The last level does not need a fade range. if (level == clipmap.NumberOfLevels - 1) { fadeRange = 0; } levelOrigin.X = Math.Min(levelOrigin.X, terrainAabbMax.X - clipmap.LevelSizes[level] + fadeRange); levelOrigin.Y = Math.Min(levelOrigin.Y, terrainAabbMax.Y - clipmap.LevelSizes[level] + fadeRange); levelOrigin.X = Math.Max(levelOrigin.X, terrainAabbMin.X - fadeRange); levelOrigin.Y = Math.Max(levelOrigin.Y, terrainAabbMin.Y - fadeRange); } // Snap to grid. levelOrigin = RoundToGrid(levelOrigin, clipmap.ActualCellSizes[level]); if (isBaseClipmap) { // To align the base clipmap with the terrain tile cell raster, we have to move it by // half a cell size. E.g. if the tile origin is (0, 0), then the first mesh vertex // is over (0, 0). The vertices should sample the clipmap texel centers. levelOrigin.X -= clipmap.ActualCellSizes[0] / 2; levelOrigin.Y -= clipmap.ActualCellSizes[0] / 2; } // Simple movement threshold for debugging: Only update clipmaps if origin // has moved more than 10 units. //if ((levelOrigin - clipmap.OldOrigins[level]).Length < 10) // continue; if (levelOrigin == clipmap.OldOrigins[level] && clipmap.UseIncrementalUpdate) { continue; } if (level < clipmap.MinLevel) { // We do not update this level. // Note: As long as the level is within the region of the next level we could still // draw it - but we ignore this here: // Set to invalid origin, to make the shader ignore this level. (The value is still used // in the shader, so we must not set it to a totally extreme value.) clipmap.Origins[level] = referencePosition2D - new Vector2F(10000); clipmap.OldOrigins[level] = clipmap.Origins[level]; continue; } clipmap.OldOrigins[level] = clipmap.Origins[level]; clipmap.OldOffsets[level] = clipmap.Offsets[level]; // Compute new toroidal wrap offset. if (!clipmap.UseIncrementalUpdate || isBaseClipmap) { // Full clipmap update needed or it is a base clipmap. clipmap.Origins[level] = levelOrigin; // Base clipmaps are usually super small (< 256²) and are updated infrequently. // Therefore we do not make a toroidal update for the base clipmap. clipmap.Offsets[level] = new Vector2F(0, 0); } else { clipmap.Origins[level] = levelOrigin; Vector2F levelSize = new Vector2F(clipmap.LevelSizes[level]); Vector2F newOffset = clipmap.OldOffsets[level] + (levelOrigin - clipmap.OldOrigins[level]) / levelSize; float cellsPerLevelWithoutBorder = clipmap.CellsPerLevel - 2 * border; //Debug.Assert( // Numeric.AreEqual(newOffset.X * cellsPerLevelWithoutBorder, // (float)Math.Floor(newOffset.X * cellsPerLevelWithoutBorder + 0.5f), 0.01f), // "New clipmap offset is not snapped to texel grid."); //Debug.Assert( // Numeric.AreEqual(newOffset.Y * cellsPerLevelWithoutBorder, // (float)Math.Floor(newOffset.Y * cellsPerLevelWithoutBorder + 0.5f), 0.01f), // "New clipmap offset is not snapped to texel grid."); // The offset should always correspond to clipmap texels, but if we compute the new offset // from the old offset then we accumulate errors. --> Snap to texels to remove error. newOffset.X = (float)Math.Floor(newOffset.X * cellsPerLevelWithoutBorder + 0.5f) / cellsPerLevelWithoutBorder; newOffset.Y = (float)Math.Floor(newOffset.Y * cellsPerLevelWithoutBorder + 0.5f) / cellsPerLevelWithoutBorder; // Use "positive modulo" to wrap to [0, 1]. newOffset.X = ((newOffset.X % 1) + 1) % 1; newOffset.Y = ((newOffset.Y % 1) + 1) % 1; Debug.Assert(Numeric.IsGreaterOrEqual(newOffset.X, 0), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsLessOrEqual(newOffset.X, 1), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsGreaterOrEqual(newOffset.Y, 0), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsLessOrEqual(newOffset.Y, 1), "New offset is not in [0,1]"); // Note: clipmap.Offsets stores the offsets as if border is 0. clipmap.Offsets[level].X = newOffset.X; clipmap.Offsets[level].Y = newOffset.Y; } } }
private void ProcessClipmap(TerrainNode node, TerrainClipmap clipmap, RenderContext context) { var graphicsDevice = context.GraphicsService.GraphicsDevice; var lodCameraNode = context.LodCameraNode ?? context.CameraNode; bool isBaseClipmap = (node.BaseClipmap == clipmap); // Update the clipmap render targets if necessary. InitializeClipmapTextures(graphicsDevice, clipmap); // Update other clipmap data (origins, offsets, ...). No rendering. // (Data is stored in TerrainClipmap class.) UpdateClipmapData(node, clipmap, lodCameraNode, isBaseClipmap); // Compute which rectangular regions need to be updated. // (Data is stored in TerrainClipmap class.) ComputeInvalidRegions(node, clipmap, isBaseClipmap); // Abort if there are no invalid regions. int numberOfInvalidRegions = 0; for (int level = 0; level < clipmap.NumberOfLevels; level++) { numberOfInvalidRegions += clipmap.InvalidRegions[level].Count; } Debug.Assert(numberOfInvalidRegions > 0 || clipmap.UseIncrementalUpdate, "If the clipmap update is not incremental, there must be at least one invalid region."); if (numberOfInvalidRegions == 0) { return; } // Set render target binding to render into all clipmap textures at once. int numberOfTextures = clipmap.Textures.Length; if (_renderTargetBindings[numberOfTextures] == null) { _renderTargetBindings[numberOfTextures] = new RenderTargetBinding[numberOfTextures]; } for (int i = 0; i < numberOfTextures; i++) { _renderTargetBindings[numberOfTextures][i] = new RenderTargetBinding((RenderTarget2D)clipmap.Textures[i]); } switch (numberOfTextures) { case 1: context.Technique = "RenderTargets1"; break; case 2: context.Technique = "RenderTargets2"; break; case 3: context.Technique = "RenderTargets3"; break; case 4: context.Technique = "RenderTargets4"; break; default: context.Technique = null; break; } graphicsDevice.SetRenderTargets(_renderTargetBindings[numberOfTextures]); // The viewport covers the whole texture atlas. var viewport = graphicsDevice.Viewport; context.Viewport = viewport; Debug.Assert(_previousMaterialBinding == null); // Loop over all layers. Render each layer into all levels (if there is an invalid region). Aabb tileAabb = new Aabb(new Vector3(-Terrain.TerrainLimit), new Vector3(Terrain.TerrainLimit)); ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, _clearLayer, tileAabb); foreach (var tile in node.Terrain.Tiles) { tileAabb = tile.Aabb; context.Object = tile; ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, tile, tileAabb); foreach (var layer in tile.Layers) { ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, layer, tileAabb); } context.Object = null; } _previousMaterialBinding = null; ClearFlags(_clearLayer, context); foreach (var tile in node.Terrain.Tiles) { ClearFlags(tile, context); foreach (var layer in tile.Layers) { ClearFlags(layer, context); } } // All invalid regions handled. for (int i = 0; i < clipmap.NumberOfLevels; i++) { clipmap.InvalidRegions[i].Clear(); } // The next time we can update incrementally. clipmap.UseIncrementalUpdate = true; }
private void ProcessLayer(GraphicsDevice graphicsDevice, RenderContext context, TerrainClipmap clipmap, bool isBaseClipmap, IInternalTerrainLayer layer, Aabb tileAabb) { // The material needs to have a "Base" or "Detail" render pass. if (!layer.Material.Contains(context.RenderPass)) return; var layerAabb = layer.Aabb ?? tileAabb; //if (!HaveContactXZ(ref layerAabb, ref clipmap.Aabb)) // continue; bool isInInvalidRegion = false; for (int level = clipmap.NumberOfLevels - 1; level >= 0; level--) { for (int i = 0; i < clipmap.InvalidRegions[level].Count; i++) { var aabb = clipmap.InvalidRegions[level][i]; if (HaveContactXZ(ref layerAabb, ref aabb)) { isInInvalidRegion = true; break; } } if (isInInvalidRegion) break; } if (!isInInvalidRegion) return; // Reset render state for each layer because layers are allowed to change render state // without restoring it in a restore pass. // The base clipmap uses no blending. The detail clipmap uses alpha-blending (but only in // RGB, A is not changed, A is used e.g. for hole info). graphicsDevice.BlendState = isBaseClipmap ? BlendState.Opaque : BlendStateAlphaBlendRgb; graphicsDevice.RasterizerState = RasterizerStateCullNoneWithScissorTest; graphicsDevice.DepthStencilState = DepthStencilState.None; #region ----- Effect binding updates ----- // Get the EffectBindings and the Effect for the current render pass. EffectBinding materialInstanceBinding = layer.MaterialInstance[context.RenderPass]; EffectBinding materialBinding = layer.Material[context.RenderPass]; EffectEx effectEx = materialBinding.EffectEx; Effect effect = materialBinding.Effect; context.MaterialInstanceBinding = materialInstanceBinding; context.MaterialBinding = materialBinding; if (effectEx.Id == 0) { effectEx.Id = 1; // Update and apply global effect parameter bindings - these bindings set the // effect parameter values for "global" parameters. For example, if an effect uses // a "ViewProjection" parameter, then a binding will compute this matrix from the // current CameraInstance in the render context and update the effect parameter. foreach (var binding in effect.GetParameterBindings()) { if (binding.Description.Hint == EffectParameterHint.Global) { binding.Update(context); binding.Apply(context); } } } // Update and apply material bindings. // If this material is the same as in the last ProcessLayer() call, then we can skip this. if (_previousMaterialBinding != materialBinding) { _previousMaterialBinding = materialBinding; if (materialBinding.Id == 0) { materialBinding.Id = 1; foreach (var binding in materialBinding.ParameterBindings) { binding.Update(context); binding.Apply(context); } } else { // The material has already been updated in this frame. foreach (var binding in materialBinding.ParameterBindings) binding.Apply(context); } } // Update and apply local, per-instance, and per-pass bindings - these are bindings // for parameters, like the "World" matrix or lighting parameters. foreach (var binding in materialInstanceBinding.ParameterBindings) { if (binding.Description.Hint != EffectParameterHint.PerPass) { binding.Update(context); binding.Apply(context); } } // Select and apply technique. var techniqueBinding = materialInstanceBinding.TechniqueBinding; techniqueBinding.Update(context); var technique = techniqueBinding.GetTechnique(effect, context); effect.CurrentTechnique = technique; #endregion int border = isBaseClipmap ? 0 : Border; // Region covered by the layer. Vector2F layerStart = new Vector2F(layerAabb.Minimum.X, layerAabb.Minimum.Z); Vector2F layerEnd = new Vector2F(layerAabb.Maximum.X, layerAabb.Maximum.Z); // Clamp to tile AABB layerStart.X = Math.Max(layerStart.X, tileAabb.Minimum.X); layerStart.Y = Math.Max(layerStart.Y, tileAabb.Minimum.Z); layerEnd.X = Math.Min(layerEnd.X, tileAabb.Maximum.X); layerEnd.Y = Math.Min(layerEnd.Y, tileAabb.Maximum.Z); // Loop over clipmap levels. for (int level = 0; level < clipmap.NumberOfLevels; level++) { // If there are no invalid regions, the combined AABB is NaN. if (Numeric.IsNaN(clipmap.CombinedInvalidRegionsAabbs[level].Minimum.X)) continue; if (layer.FadeInStart > level || layer.FadeOutEnd <= level) continue; if (!HaveContactXZ(ref layerAabb, ref clipmap.CombinedInvalidRegionsAabbs[level])) continue; Vector2F levelOrigin = clipmap.Origins[level]; Vector2F levelSize = new Vector2F(clipmap.LevelSizes[level]); // without border! Vector2F levelEnd = levelOrigin + levelSize; float cellsPerLevelWithoutBorder = clipmap.CellsPerLevel - 2 * border; float cellSize = clipmap.ActualCellSizes[level]; float texelsPerUnit = 1 / cellSize; // Following effect parameters change per clipmap level: UpdateAndApplyParameter(materialBinding, "TerrainClipmapLevel", (float)level, context); UpdateAndApplyParameter(materialBinding, "TerrainClipmapCellSize", cellSize, context); // The rectangle of the whole clipmap level (including border). var levelRect = GetScreenSpaceRectangle(clipmap, level); // The pixel position of the offset. int offsetX = levelRect.X + border + (int)(cellsPerLevelWithoutBorder * clipmap.Offsets[level].X + 0.5f); int offsetY = levelRect.Y + border + (int)(cellsPerLevelWithoutBorder * clipmap.Offsets[level].Y + 0.5f); // Handle the 4 rectangles of the toroidally wrapped clipmap. bool applyPass = true; for (int i = 0; i < 4; i++) { Rectangle quadrantRect; Vector2F offsetPosition; switch (i) { case 0: // Top left rectangle. quadrantRect = new Rectangle(levelRect.X, levelRect.Y, offsetX - levelRect.X, offsetY - levelRect.Y); offsetPosition = levelEnd; break; case 1: // Top right rectangle. quadrantRect = new Rectangle(offsetX, levelRect.Y, levelRect.Right - offsetX, offsetY - levelRect.Y); offsetPosition.X = levelOrigin.X; offsetPosition.Y = levelEnd.Y; break; case 2: // Bottom left rectangle. quadrantRect = new Rectangle(levelRect.X, offsetY, offsetX - levelRect.X, levelRect.Bottom - offsetY); offsetPosition.X = levelEnd.X; offsetPosition.Y = levelOrigin.Y; break; default: // Bottom right rectangle. quadrantRect = new Rectangle(offsetX, offsetY, levelRect.Right - offsetX, levelRect.Bottom - offsetY); offsetPosition = levelOrigin; break; } if (quadrantRect.Width == 0 || quadrantRect.Height == 0) continue; applyPass |= UpdateAndApplyParameter(materialBinding, "TerrainClipmapOffsetWorld", (Vector2)offsetPosition, context); applyPass |= UpdateAndApplyParameter(materialBinding, "TerrainClipmapOffsetScreen", new Vector2(offsetX, offsetY), context); var passBinding = techniqueBinding.GetPassBinding(technique, context); foreach (var pass in passBinding) { // Update and apply per-pass bindings. foreach (var binding in materialInstanceBinding.ParameterBindings) { if (binding.Description.Hint == EffectParameterHint.PerPass) { binding.Update(context); binding.Apply(context); applyPass = true; } } if (applyPass) { pass.Apply(); applyPass = false; } foreach (var aabb in clipmap.InvalidRegions[level]) { // Intersect layer AABB with invalid region AABB. Vector2F clippedLayerStart, clippedLayerEnd; clippedLayerStart.X = Math.Max(layerStart.X, aabb.Minimum.X); clippedLayerStart.Y = Math.Max(layerStart.Y, aabb.Minimum.Z); clippedLayerEnd.X = Math.Min(layerEnd.X, aabb.Maximum.X); clippedLayerEnd.Y = Math.Min(layerEnd.Y, aabb.Maximum.Z); // Nothing to do if layer AABB does not intersect invalid region. if (clippedLayerStart.X >= clippedLayerEnd.X || clippedLayerStart.Y >= clippedLayerEnd.Y) continue; // Compute screen space rectangle of intersection (relative to toroidal offset). var invalidRect = GetScissorRectangle( clippedLayerStart - offsetPosition, clippedLayerEnd - clippedLayerStart, texelsPerUnit); // Add toroidal offset screen position. invalidRect.X += offsetX; invalidRect.Y += offsetY; // Set a scissor rectangle to avoid drawing outside the current toroidal wrap // part and outside the invalid region. var scissorRect = Rectangle.Intersect(quadrantRect, invalidRect); if (scissorRect.Width <= 0 || scissorRect.Height <= 0) continue; graphicsDevice.ScissorRectangle = scissorRect; // Compute world space position of scissor rectangle corners. Vector2F start, end; start.X = offsetPosition.X + (scissorRect.X - offsetX) * cellSize; start.Y = offsetPosition.Y + (scissorRect.Y - offsetY) * cellSize; end.X = offsetPosition.X + (scissorRect.Right - offsetX) * cellSize; end.Y = offsetPosition.Y + (scissorRect.Bottom - offsetY) * cellSize; Debug.Assert(Numeric.IsLessOrEqual(start.X, end.X)); Debug.Assert(Numeric.IsLessOrEqual(start.Y, end.Y)); layer.OnDraw(graphicsDevice, scissorRect, start, end); } } } } }
private void ProcessClipmap(TerrainNode node, TerrainClipmap clipmap, RenderContext context) { var graphicsDevice = context.GraphicsService.GraphicsDevice; var lodCameraNode = context.LodCameraNode ?? context.CameraNode; bool isBaseClipmap = (node.BaseClipmap == clipmap); // Update the clipmap render targets if necessary. InitializeClipmapTextures(graphicsDevice, clipmap); // Update other clipmap data (origins, offsets, ...). No rendering. // (Data is stored in TerrainClipmap class.) UpdateClipmapData(node, clipmap, lodCameraNode, isBaseClipmap); // Compute which rectangular regions need to be updated. // (Data is stored in TerrainClipmap class.) ComputeInvalidRegions(node, clipmap, isBaseClipmap); // Abort if there are no invalid regions. int numberOfInvalidRegions = 0; for (int level = 0; level < clipmap.NumberOfLevels; level++) numberOfInvalidRegions += clipmap.InvalidRegions[level].Count; Debug.Assert(numberOfInvalidRegions > 0 || clipmap.UseIncrementalUpdate, "If the clipmap update is not incremental, there must be at least one invalid region."); if (numberOfInvalidRegions == 0) return; // Set render target binding to render into all clipmap textures at once. int numberOfTextures = clipmap.Textures.Length; if (_renderTargetBindings[numberOfTextures] == null) _renderTargetBindings[numberOfTextures] = new RenderTargetBinding[numberOfTextures]; for (int i = 0; i < numberOfTextures; i++) _renderTargetBindings[numberOfTextures][i] = new RenderTargetBinding((RenderTarget2D)clipmap.Textures[i]); switch (numberOfTextures) { case 1: context.Technique = "RenderTargets1"; break; case 2: context.Technique = "RenderTargets2"; break; case 3: context.Technique = "RenderTargets3"; break; case 4: context.Technique = "RenderTargets4"; break; default: context.Technique = null; break; } graphicsDevice.SetRenderTargets(_renderTargetBindings[numberOfTextures]); // The viewport covers the whole texture atlas. var viewport = graphicsDevice.Viewport; context.Viewport = viewport; Debug.Assert(_previousMaterialBinding == null); // Loop over all layers. Render each layer into all levels (if there is an invalid region). Aabb tileAabb = new Aabb(new Vector3F(-Terrain.TerrainLimit), new Vector3F(Terrain.TerrainLimit)); ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, _clearLayer, tileAabb); foreach (var tile in node.Terrain.Tiles) { tileAabb = tile.Aabb; context.Object = tile; ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, tile, tileAabb); foreach (var layer in tile.Layers) ProcessLayer(graphicsDevice, context, clipmap, isBaseClipmap, layer, tileAabb); context.Object = null; } _previousMaterialBinding = null; ClearFlags(_clearLayer, context); foreach (var tile in node.Terrain.Tiles) { ClearFlags(tile, context); foreach (var layer in tile.Layers) ClearFlags(layer, context); } // All invalid regions handled. for (int i = 0; i < clipmap.NumberOfLevels; i++) clipmap.InvalidRegions[i].Clear(); // The next time we can update incrementally. clipmap.UseIncrementalUpdate = true; }
private void ComputeInvalidRegions(TerrainNode node, TerrainClipmap clipmap, bool isBaseClipmap) { // Note: // This method computes the AABBs of terrain parts that need to be updated. // Important: AABBs must not overlap because the materials can use alpha blending and they // must not blend twice into the same area. var terrain = node.Terrain; var invalidRegions = isBaseClipmap ? terrain.InvalidBaseRegions : terrain.InvalidDetailRegions; var areInvalidRegionsClipped = isBaseClipmap ? terrain.AreInvalidBaseRegionsClipped : terrain.AreInvalidDetailRegionsClipped; float border = isBaseClipmap ? 0 : Border; for (int level = 0; level < clipmap.NumberOfLevels; level++) { if (clipmap.InvalidRegions[level] == null) clipmap.InvalidRegions[level] = new List<Aabb>(); // Compute AABB of whole level. float borderWorld = border * clipmap.ActualCellSizes[level]; Vector2F newOrigin = clipmap.Origins[level]; var aabb = new Aabb { Minimum = { X = newOrigin.X - borderWorld, Y = float.MinValue, Z = newOrigin.Y - borderWorld }, Maximum = { X = newOrigin.X + clipmap.LevelSizes[level] + borderWorld, Y = float.MaxValue, Z = newOrigin.Y + clipmap.LevelSizes[level] + borderWorld } }; // Store AABB of whole clipmap. if (level == clipmap.NumberOfLevels - 1) clipmap.Aabb = aabb; // For debugging: //var oldOrigin = clipmap.OldOrigins[level]; //if (oldOrigin != newOrigin) // clipmap.InvalidRegions[level].Add(aabb); if (!clipmap.UseIncrementalUpdate) { // Incremental update not possible. --> The whole clipmap is the invalid region. clipmap.InvalidRegions[level].Add(aabb); } else { // The clipmap contains info from the last frame and the layers where not changed. // We can use a toroidal update. Vector2F oldOrigin = clipmap.OldOrigins[level]; float levelSize = clipmap.LevelSizes[level]; if (oldOrigin != newOrigin) { // The camera or the clipmap level origin has moved. if (oldOrigin.X >= newOrigin.X + levelSize || oldOrigin.Y >= newOrigin.Y + levelSize || newOrigin.X >= oldOrigin.X + levelSize || newOrigin.Y >= oldOrigin.Y + levelSize || isBaseClipmap) // Base clipmap does not (yet) use toroidal wrap. { // We moved a lot and we cannot reuse anything from the last frame. clipmap.InvalidRegions[level].Clear(); clipmap.InvalidRegions[level].Add(aabb); clipmap.Offsets[level] = new Vector2F(0); } else { // The origin has moved. An L shape has to be updated. --> We need two AABBs. // To avoid an unnecessary overlap: The horizontal AABB covers the whole width. // The vertical AABB is shorter to avoid overlap with the horizontal AABB. Aabb horizontalAabb = new Aabb(); horizontalAabb.Minimum.X = newOrigin.X; horizontalAabb.Maximum.X = newOrigin.X + levelSize; horizontalAabb.Minimum.Y = float.MinValue; horizontalAabb.Maximum.Y = float.MaxValue; if (oldOrigin.Y <= newOrigin.Y) { // Origin moved down. horizontalAabb.Minimum.Z = oldOrigin.Y + levelSize; horizontalAabb.Maximum.Z = newOrigin.Y + levelSize; } else { // Origin moved up. horizontalAabb.Minimum.Z = newOrigin.Y; horizontalAabb.Maximum.Z = oldOrigin.Y; } Aabb verticalAabb = new Aabb(); verticalAabb.Minimum.Y = float.MinValue; verticalAabb.Maximum.Y = float.MaxValue; verticalAabb.Minimum.Z = newOrigin.Y; verticalAabb.Maximum.Z = newOrigin.Y + levelSize; if (oldOrigin.X <= newOrigin.X) { // Origin moved right. verticalAabb.Minimum.X = oldOrigin.X + levelSize; verticalAabb.Maximum.X = newOrigin.X + levelSize; } else { // Origin moved left. verticalAabb.Minimum.X = newOrigin.X; verticalAabb.Maximum.X = oldOrigin.X; } Debug.Assert(horizontalAabb.Minimum <= horizontalAabb.Maximum); Debug.Assert(verticalAabb.Minimum <= verticalAabb.Maximum); Debug.Assert(Numeric.AreEqual(horizontalAabb.Extent.X, levelSize)); // (Assertions need larger epsilon.) //Debug.Assert(Numeric.AreEqual(horizontalAabb.Extent.Z, Math.Abs(oldOrigin.Y - newOrigin.Y))); //Debug.Assert(Numeric.AreEqual(verticalAabb.Extent.X, Math.Abs(oldOrigin.X - newOrigin.X))); // Add the border for texture filtering. horizontalAabb.Minimum.X -= borderWorld; horizontalAabb.Minimum.Z -= borderWorld; horizontalAabb.Maximum.X += borderWorld; horizontalAabb.Maximum.Z += borderWorld; verticalAabb.Minimum.X -= borderWorld; verticalAabb.Minimum.Z -= borderWorld; verticalAabb.Maximum.X += borderWorld; verticalAabb.Maximum.Z += borderWorld; // AABBs must not overlap. if (oldOrigin.Y <= newOrigin.Y) verticalAabb.Maximum.Z = horizontalAabb.Minimum.Z; else verticalAabb.Minimum.Z = horizontalAabb.Maximum.Z; Debug.Assert(horizontalAabb.Minimum.X >= verticalAabb.Maximum.X || horizontalAabb.Maximum.X <= verticalAabb.Minimum.X || horizontalAabb.Minimum.Y >= verticalAabb.Maximum.Y || horizontalAabb.Maximum.Y <= verticalAabb.Minimum.Y || horizontalAabb.Minimum.Z >= verticalAabb.Maximum.Z || horizontalAabb.Maximum.Z <= verticalAabb.Minimum.Z, "Invalid region AABBs must not overlap."); if (clipmap.InvalidRegions[level].Count == 0) { clipmap.InvalidRegions[level].Add(horizontalAabb); clipmap.InvalidRegions[level].Add(verticalAabb); } else { AddAabbWithClipping(clipmap.InvalidRegions[level], ref horizontalAabb); AddAabbWithClipping(clipmap.InvalidRegions[level], ref verticalAabb); } } } if (!areInvalidRegionsClipped && invalidRegions.Count > 1) { // Clip the invalid regions stored in the Terrain once. _aabbList.Clear(); for (int i = 0; i < invalidRegions.Count; i++) { var invalidAabb = invalidRegions[i]; AddAabbWithClipping(_aabbList, ref invalidAabb); } invalidRegions.Clear(); invalidRegions.AddRange(_aabbList); // Update flag (also in terrain in case other TerrainNodes use the same Terrain). areInvalidRegionsClipped = true; if (isBaseClipmap) terrain.AreInvalidBaseRegionsClipped = true; else terrain.AreInvalidDetailRegionsClipped = true; } if (invalidRegions.Count > 0) { // Add all invalid regions - without overlap. for (int i = 0; i < invalidRegions.Count; i++) { var invalidAabb = invalidRegions[i]; AddAabbWithClipping(clipmap.InvalidRegions[level], ref invalidAabb); } } } #if DEBUG // Assert: Invalid regions do not overlap. for (int i = 0; i < clipmap.InvalidRegions[level].Count; i++) { var a = clipmap.InvalidRegions[level][i]; for (int j = i + 1; j < clipmap.InvalidRegions[level].Count; j++) { var b = clipmap.InvalidRegions[level][j]; Debug.Assert(!HaveContactXZ(ref a, ref b)); } } #endif // Compute the union of all invalid regions. We can use this to early out if a layer // does not overlap this combined region. var numberOfInvalidRegions = clipmap.InvalidRegions[level].Count; if (numberOfInvalidRegions == 0) { clipmap.CombinedInvalidRegionsAabbs[level] = new Aabb(new Vector3F(float.NaN), new Vector3F(float.NaN)); } else { clipmap.CombinedInvalidRegionsAabbs[level] = clipmap.InvalidRegions[level][0]; for (int i = 1; i < numberOfInvalidRegions; i++) clipmap.CombinedInvalidRegionsAabbs[level].Grow(clipmap.InvalidRegions[level][i]); } // For debugging: //clipmap.CombinedInvalidRegionsAabbs[level] = new Aabb(new Vector3F(float.MinValue), new Vector3F(float.MaxValue)); } }
private static void UpdateClipmapData(TerrainNode node, TerrainClipmap clipmap, CameraNode lodCameraNode, bool isBaseClipmap) { int border = isBaseClipmap ? 0 : Border; var terrain = node.Terrain; var terrainAabb = terrain.Aabb; Vector2F terrainAabbMin = new Vector2F(terrainAabb.Minimum.X, terrainAabb.Minimum.Z); Vector2F terrainAabbMax = new Vector2F(terrainAabb.Maximum.X, terrainAabb.Maximum.Z); for (int level = clipmap.NumberOfLevels - 1; level >= 0; level--) { // Compute new origins. int texelsPerLevel = clipmap.CellsPerLevel - 2 * border; Vector3F referencePosition3D = lodCameraNode.PoseWorld.Position; Vector2F referencePosition2D = new Vector2F(referencePosition3D.X, referencePosition3D.Z); clipmap.LevelSizes[level] = clipmap.ActualCellSizes[level] * texelsPerLevel; Vector2F levelOrigin = new Vector2F( referencePosition2D.X - clipmap.LevelSizes[level] / 2, referencePosition2D.Y - clipmap.LevelSizes[level] / 2); // Do not move detail clipmap outside the terrain. This would only waste resources. if (!isBaseClipmap) { // The fade range is hardcoded to 15% of the radius in Terrain.fxh. float fadeRange = clipmap.LevelSizes[level] * node.DetailFadeRange / 2; // / 2 because its relative to the radius. // The last level does not need a fade range. if (level == clipmap.NumberOfLevels - 1) fadeRange = 0; levelOrigin.X = Math.Min(levelOrigin.X, terrainAabbMax.X - clipmap.LevelSizes[level] + fadeRange); levelOrigin.Y = Math.Min(levelOrigin.Y, terrainAabbMax.Y - clipmap.LevelSizes[level] + fadeRange); levelOrigin.X = Math.Max(levelOrigin.X, terrainAabbMin.X - fadeRange); levelOrigin.Y = Math.Max(levelOrigin.Y, terrainAabbMin.Y - fadeRange); } // Snap to grid. levelOrigin = RoundToGrid(levelOrigin, clipmap.ActualCellSizes[level]); if (isBaseClipmap) { // To align the base clipmap with the terrain tile cell raster, we have to move it by // half a cell size. E.g. if the tile origin is (0, 0), then the first mesh vertex // is over (0, 0). The vertices should sample the clipmap texel centers. levelOrigin.X -= clipmap.ActualCellSizes[0] / 2; levelOrigin.Y -= clipmap.ActualCellSizes[0] / 2; } // Simple movement threshold for debugging: Only update clipmaps if origin // has moved more than 10 units. //if ((levelOrigin - clipmap.OldOrigins[level]).Length < 10) // continue; if (levelOrigin == clipmap.OldOrigins[level] && clipmap.UseIncrementalUpdate) continue; if (level < clipmap.MinLevel) { // We do not update this level. // Note: As long as the level is within the region of the next level we could still // draw it - but we ignore this here: // Set to invalid origin, to make the shader ignore this level. (The value is still used // in the shader, so we must not set it to a totally extreme value.) clipmap.Origins[level] = referencePosition2D - new Vector2F(10000); clipmap.OldOrigins[level] = clipmap.Origins[level]; continue; } clipmap.OldOrigins[level] = clipmap.Origins[level]; clipmap.OldOffsets[level] = clipmap.Offsets[level]; // Compute new toroidal wrap offset. if (!clipmap.UseIncrementalUpdate || isBaseClipmap) { // Full clipmap update needed or it is a base clipmap. clipmap.Origins[level] = levelOrigin; // Base clipmaps are usually super small (< 256²) and are updated infrequently. // Therefore we do not make a toroidal update for the base clipmap. clipmap.Offsets[level] = new Vector2F(0, 0); } else { clipmap.Origins[level] = levelOrigin; Vector2F levelSize = new Vector2F(clipmap.LevelSizes[level]); Vector2F newOffset = clipmap.OldOffsets[level] + (levelOrigin - clipmap.OldOrigins[level]) / levelSize; float cellsPerLevelWithoutBorder = clipmap.CellsPerLevel - 2 * border; //Debug.Assert( // Numeric.AreEqual(newOffset.X * cellsPerLevelWithoutBorder, // (float)Math.Floor(newOffset.X * cellsPerLevelWithoutBorder + 0.5f), 0.01f), // "New clipmap offset is not snapped to texel grid."); //Debug.Assert( // Numeric.AreEqual(newOffset.Y * cellsPerLevelWithoutBorder, // (float)Math.Floor(newOffset.Y * cellsPerLevelWithoutBorder + 0.5f), 0.01f), // "New clipmap offset is not snapped to texel grid."); // The offset should always correspond to clipmap texels, but if we compute the new offset // from the old offset then we accumulate errors. --> Snap to texels to remove error. newOffset.X = (float)Math.Floor(newOffset.X * cellsPerLevelWithoutBorder + 0.5f) / cellsPerLevelWithoutBorder; newOffset.Y = (float)Math.Floor(newOffset.Y * cellsPerLevelWithoutBorder + 0.5f) / cellsPerLevelWithoutBorder; // Use "positive modulo" to wrap to [0, 1]. newOffset.X = ((newOffset.X % 1) + 1) % 1; newOffset.Y = ((newOffset.Y % 1) + 1) % 1; Debug.Assert(Numeric.IsGreaterOrEqual(newOffset.X, 0), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsLessOrEqual(newOffset.X, 1), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsGreaterOrEqual(newOffset.Y, 0), "New offset is not in [0,1]"); Debug.Assert(Numeric.IsLessOrEqual(newOffset.Y, 1), "New offset is not in [0,1]"); // Note: clipmap.Offsets stores the offsets as if border is 0. clipmap.Offsets[level].X = newOffset.X; clipmap.Offsets[level].Y = newOffset.Y; } } }