private static void RasterizeBlockingSplines(TerrainComponent terrain, TerrainVegetationComponent component, PixelBuffer mask, int maskChannel, float terrainOffset) { foreach (var spline in component.BlockingSplines) { var vertices = new FastList <Splines.VertexPositionNormalTangentColorTexture>(); var indices = new FastList <int>(); Splines.SplineMeshBuilder.CreateSplineMesh(spline, vertices, indices); for (var i = 0; i < indices.Count; i += 3) { var vt1f = vertices[indices[i + 0]].Position; var vt2f = vertices[indices[i + 1]].Position; var vt3f = vertices[indices[i + 2]].Position; vt1f = (vt1f + terrainOffset) / terrain.Size * mask.Width; vt2f = (vt2f + terrainOffset) / terrain.Size * mask.Width; vt3f = (vt3f + terrainOffset) / terrain.Size * mask.Width; // Just ignore Y axis, works well enoguh for splines but if we // support generic volumes in the future then we might want to project it a bit more properly // or maybe just do an offscreen render pass on the gpu ... var vt1 = new Int2((int)vt1f.X, (int)vt1f.Z); var vt2 = new Int2((int)vt2f.X, (int)vt2f.Z); var vt3 = new Int2((int)vt3f.X, (int)vt3f.Z); // Calculate bounding box var maxX = Math.Max(vt1.X, Math.Max(vt2.X, vt3.X)); var minX = Math.Min(vt1.X, Math.Min(vt2.X, vt3.X)); var maxY = Math.Max(vt1.Y, Math.Max(vt2.Y, vt3.Y)); var minY = Math.Min(vt1.Y, Math.Min(vt2.Y, vt3.Y)); // Triangle intersection var vs1 = vt2 - vt1; var vs2 = vt3 - vt1; for (var x = minX; x <= maxX; x++) { for (var y = minY; y < maxY; y++) { var q = new Int2(x - vt1.X, y - vt1.Y); var s = (float)CrossProduct(q, vs2) / CrossProduct(vs1, vs2); var t = (float)CrossProduct(vs1, q) / CrossProduct(vs1, vs2); if (s >= 0 && t >= 0 && (s + t) <= 1) { var currentPixel = mask.GetPixel <Color>(x, y); currentPixel[maskChannel] = 0; mask.SetPixel <Color>(x, y, currentPixel); } } } } } }
/// <summary> /// Update and recreate a page if necessary /// </summary> private void UpdatePages(TerrainComponent terrain, TerrainVegetationComponent component, TerrainVegetationRenderData renderData) { if (renderData.Pages != null && renderData.MaskImage != null && !component.IsDirty) { return; } if (renderData.MaskImage == null || renderData.Mask != component.Mask) { renderData.Mask = component.Mask; renderData.MaskImage?.Dispose(); // Get mask image data try { var game = Services.GetService <IGame>(); var graphicsContext = game.GraphicsContext; var commandList = graphicsContext.CommandList; renderData.MaskImage = component.Mask.GetDataAsImage(commandList); } catch { // Image probably not loaded yet .. try again next frame :) return; } } // Cache render data so we won't need to recreate pages var mask = renderData.MaskImage.PixelBuffer[0]; var maskChannel = (int)component.MaskChannel; // Calculate terrain center offset var terrainOffset = terrain.Size / 2.0f; RasterizeBlockingSplines(terrain, component, mask, maskChannel, terrainOffset); // Create vegetation pages var rng = new Random(component.Seed); var pagesPerRow = (int)terrain.Size / PageSize; var instancesPerRow = (int)(PageSize * component.Density); var distancePerInstance = PageSize / (float)instancesPerRow; renderData.Pages = new TerrainVegetationPage[pagesPerRow * pagesPerRow]; var scaleRange = component.MaxScale - component.MinScale; for (var pz = 0; pz < pagesPerRow; pz++) { for (var px = 0; px < pagesPerRow; px++) { var radius = PageSize * 0.5f; var pagePosition = new Vector3(px * PageSize - terrainOffset, 0, pz * PageSize - terrainOffset); var page = new TerrainVegetationPage(); renderData.Pages[pz * pagesPerRow + px] = page; for (var iz = 0; iz < instancesPerRow; iz++) { for (var ix = 0; ix < instancesPerRow; ix++) { var position = pagePosition; position.X += ix * distancePerInstance; position.Z += iz * distancePerInstance; position.X += (float)rng.NextDouble() * distancePerInstance * 2.0f - distancePerInstance; position.Z += (float)rng.NextDouble() * distancePerInstance * 2.0f - distancePerInstance; position.Y = terrain.GetHeightAt(position.X, position.Z); var tx = (int)((position.X + terrainOffset) / terrain.Size * mask.Width); var ty = (int)((position.Z + terrainOffset) / terrain.Size * mask.Height); if (tx < 0 || tx >= mask.Width || ty < 0 || ty >= mask.Height) { continue; } var maskDensity = mask.GetPixel <Color>(tx, ty)[maskChannel] / 255.0; if (rng.NextDouble() > maskDensity) { continue; } var normal = terrain.GetNormalAt(position.X, position.Z); var slope = 1.0f - Math.Abs(normal.Y); if (slope < component.MinSlope || slope > component.MaxSlope) { continue; } var scale = (float)rng.NextDouble() * scaleRange + component.MinScale; var rotation = Quaternion.RotationAxis(Vector3.UnitY, (float)rng.NextDouble() * MathUtil.TwoPi) * Quaternion.BetweenDirections(Vector3.UnitY, normal); var scaling = new Vector3(scale); Matrix.Transformation(ref scaling, ref rotation, ref position, out var transformation); page.Instances.Add(transformation); } } page.WorldPosition = pagePosition + new Vector3(radius, 0, radius); } } component.IsDirty = false; }
public bool IsDirty(TerrainComponent component) => Material != component.Material || Size != component.Size || Heightmap != component.Heightmap || Mesh == null;
public void Update(TerrainComponent component) { Size = component.Size; Material = component.Material; Heightmap = component.Heightmap; }