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;
        }
예제 #3
0
 public bool IsDirty(TerrainComponent component)
 => Material != component.Material || Size != component.Size || Heightmap != component.Heightmap || Mesh == null;
예제 #4
0
 public void Update(TerrainComponent component)
 {
     Size      = component.Size;
     Material  = component.Material;
     Heightmap = component.Heightmap;
 }