using System.Collections; using System.Collections.Generic; using Godot; using System; using System.Threading.Tasks; namespace Rokojori { /** Creates a grass patch 3D model asset. The GrassPatch has various settings to create a different styles of grass. It allows to change the shapes of the blades, their number and distribution, their triangle count, rotation and scale, LOD levels and much more. */ [Tool] [GlobalClass, Icon("res://addons/rokojori_action_library/Icons/Scatterer.svg") ] public partial class GrassPatch:Node3D { [ExportToolButton( "(?) Help", Icon = "Help") ] public Callable openHelpButton => Callable.From( ()=> { #if TOOLS Rokojori.Tools.OnlineDocs.Open( GetType() ); #endif } ); /** The output where the mesh is stored, gets generated automatically.*/ [Export] public MeshInstance3D output; /** The seed for the randomizers*/ [Export] public int seed = 1984; [ExportToolButton( "Create")] public Callable createButton => Callable.From( ()=> { CreatePatch(); } ); /** The material the grass will be assigned*/ [Export] public Material material; /** Patch size for x and z*/ [ExportGroup( "Patch")] [Export] public float patchSize = 2; /** Additional patch size for x*/ [Export] public float patchSizeX = 0; /** Additional patch size for z*/ [Export] public float patchSizeZ = 0; /** Patch offset in x/z*/ [Export] public Vector2 patchOffsetPosition = new Vector2( 0.5f, 0.5f ); /** If centerPatch is on, the patch will be centered automatically*/ [Export] public bool centerPatch = false; /** If on, this determines which vertices should be counted to compute the center of the patch.

All vertices in y that are below, will not count to compute the center. This can be usefull to align more to the upper more visible parts, it usually has only a small impact.

*/ [Export] public float centerPatchComputationHeightTreshold = 0.1f; [ExportGroup( "Blades")] /** The number of blades it will generate in x and z*/ [Export( PropertyHint.Range, "0,100")] public int blades = 20; /** Additional number of blades it will generate in x*/ [Export( PropertyHint.Range, "0,100")] public int bladesX = 0; /** Additional number of blades it will generate in z*/ [Export( PropertyHint.Range, "0,100")] public int bladesZ = 0; /** This is an output/debug property, and will show how many blades were created*/ [Export] public int X_numBlades; /** This defines the number of segments (vertical divisions) for each blade.*/ [ExportGroup( "Triangles")] [Export( PropertyHint.Range, "1,256")] public int bladeSegments = 3; /** This generates back faces, if it is disabled the material should not use backface culling*/ [Export] public bool createBackFaces = true; /** If on, this allows the ends of the blades to be merged as triangles instead of quads.

This is usefull for creating very low poly grass blades with only one triangle. The width of the blade needs to be very close to zero at the start or end to be applied.

*/ [Export] public bool allowTrianglesOnEnds = true; /** This is an output/debug property, and will show how many triangels were created*/ [Export] public int X_numTriangles; /** Allows to use a non-linear distortion for the segmentation.

Normally, the blade shapes are segmented by taking an equal distance of divisions throughout the height of the blade. But since this low poly shapes won't be represented well with this approach, zhis setting allows to distort the segmentation, some shapes are better represented with it.

*/ [Export] public Curve bladeSegmentMapping = MathX.Curve( 0, 1 ); /** The number of splits in U for the blades, when generating the uv coordinates.

This allows to use multiple regions for the blades, so that blades can have different material settings.

*/ [ExportGroup( "Segmentation")] [Export] public int uvSegmentColumns = 1; /** The number of splits in V for the blades, when generating the uv coordinates.

This allows to use multiple regions for the blades, so that blades can have different material settings.

*/ [Export] public int uvSegmentRows = 1; /** The distribution for the UV region selection.

This will change the propability of the uv regions, so that some regions can be used more often than other regions.

*/ [Export] public Curve uvSegmentWeightsClose = MathX.Curve( 1f ); /** The distribution for the UV region selection for blades outside the center.

This will change the propability of the uv regions outside the center. This allows to have different distribution dependening on the position.

*/ [Export] public Curve uvSegmentWeightsFar = null; /** Blending factor between undistorted and distorted U mapping.

This warps/pulls the strength of the close or far distribution.

*/ [Export( PropertyHint.Range, "0,1" )] public float uvSegmentDistortion = 0.5f; /** Defines how far the U mapping can distort. */ [Export( PropertyHint.Range, "0,0.5" )] public float uvSegmentMaxRange = 0.3f; /** Defines the height of the blades, the curve defines the random distribution.

The total y size is sum of the blade height and baldInGround.

*/ [ExportGroup( "Shape")] [Export] public Curve bladeHeight = MathX.Curve( 0.4f ); /** The amount the blade is continued below 0 in y. See also bladeHeight. */ [Export] public Curve bladeInGround = MathX.Curve( 0.05f ); /** The width of the blade defined over its height. */ [Export] public Curve bladeWidth = MathX.Curve( 0.05f ); /** Optional: A second width (as randomizer) of the blade defined over its height.

The randomizer chooses a blend value before the creation and will use the resulting curve as width. This means it will not randomly jump between the first and second width, but uses an interpolated curve.

*/ [Export] public Curve bladeWidth2 = null; [ExportGroup( "Curve")] [Export] public Curve bladeBending = MathX.Curve( 0f ); [Export] public Curve bladeBending2 = null; [Export] public Curve bladeTwisting = null; [Export] public Curve bladeTwisting2 = null; [Export] public Curve rolling = MathX.Curve( 0f ); [Export] public Curve rolling2 = null; [ExportGroup( "Offset")] [Export] public Curve positionJitter = MathX.Curve( 0.05f ); [Export] public Curve positionJitterX = MathX.Curve( 0.05f ); [Export] public Curve positionJitterZ = MathX.Curve( 0.05f ); [ExportGroup( "Scale")] [Export] public Curve bladeScale = MathX.Curve( 1f ); [Export] public Curve scaleByDistanceX = MathX.Curve( 1f ); [Export] public Curve scaleByDistanceZ = MathX.Curve( 1f ); [Export] public Curve xRemapper = null; [Export] public Curve yRemapper = null; [Export] public Curve zRemapper = null; [Export] public Curve scaleXZForY = null; [Export] public Curve scaleXForY = null; [Export] public Curve scaleZForY = null; [ExportGroup( "Rotation")] [Export] public Curve yawRotation = MathX.Curve( 0f, 1f ); [Export] public Curve randomRotation = MathX.Curve( 0f, 20f ); [ExportGroup( "Noise")] [Export] public Curve vertexTurbulenceAmount = MathX.Curve( 0f, 0f ); [Export] public Curve vertexTurbulenceScale = MathX.Curve( 1f, 1f ); [Export] public Curve vertexTurbulenceScaleX = MathX.Curve( 1f, 1f ); [Export] public Curve vertexTurbulenceScaleY = MathX.Curve( 1f, 1f ); [Export] public Curve vertexTurbulenceScaleZ = MathX.Curve( 1f, 1f ); [ExportGroup( "Normals")] [Export] public Curve normalBlending = MathX.Curve( 0.5f ); [Export] public Curve normalBlendingAmountOverY = MathX.Curve( 1f ); [Export] public Vector3 normalBlendingDirection = Vector3.Up; [ExportGroup( "Filter")] [Export (PropertyHint.Range, "0,1")] public float filterTreshold = 1; [Export] public Vector2 filterScale = new Vector2( 1, 1 ); [Export] public Vector2 filterOffset = new Vector2( 0, 0 ); [Export] public Vector2 positionToFilter = new Vector2( 0, 0 ); [ExportGroup("LOD Levels")] [Export] public GrassPatchLODLevel[] lodLevels = new GrassPatchLODLevel[ 0 ]; [Export] public int currentLODLevel = -1; [Export] public bool generateLODMesh = false; [Export] public float customLODEdgeLength = 0; [Export] public int lodLerpSteps = 2; [Export] public Curve lowCurve = MathX.Curve( 0, 1 ); [Export] public Curve highCurve = MathX.Curve( 0, 1 ); float _maxWidth = 0; Vector3 _patchOffset = Vector3.Zero; public int ComputeNumBlades() { if ( blades == 0 ) { if ( bladesX < bladesZ ) { bladesX = Mathf.Max( 1, bladesX ); } else { bladesZ = Mathf.Max( 1, bladesZ ); } } return ( bladesX + blades ) * ( blades + bladesZ ); } public int ComputeNumTriangles() { return bladeSegments * 2 * ComputeNumBlades(); } public async Task CreatePatch() { if ( blades == 0 && bladesX == 0 && bladesZ == 0) { return; } if ( blades == 0 ) { if ( bladesX < bladesZ ) { bladesX = Mathf.Max( 1, bladesX ); } else { bladesZ = Mathf.Max( 1, bladesZ ); } } if ( lodLevels == null ) { lodLevels = new GrassPatchLODLevel[]{}; } this.DestroyChildren(); this.output = this.CreateChild( "Grass Patch Mesh" ); var random = new LCG(); random.SetSeed( 1712 + seed ); _patchOffset = new Vector3( - ( patchSizeX + patchSize ) * patchOffsetPosition.X, 0, - ( patchSizeZ + patchSize ) * patchOffsetPosition.Y ); _maxWidth = MathX.CurveMaximum( bladeWidth ); X_numBlades = 0; if ( ! generateLODMesh ) { var mg = CreatePatchGeometry(); X_numTriangles = mg.indices.Count / 3; output.Mesh = mg.GenerateMesh(); output.Mesh.SurfaceSetMaterial( 0, material ); } else { var cachedCurrentLODLevel = currentLODLevel; try { var mgs = new List(); var numTris = 0; var time = this.StartAsyncTimer(); var edgeLength = ( MathX.CurveAverage( bladeHeight ) * MathX.CurveAverage( bladeScale ) ) / bladeSegments; if ( customLODEdgeLength > 0 ) { edgeLength = customLODEdgeLength; } this.LogInfo( "Edge Length", edgeLength ); for ( int i = 0; i < lodLevels.Length + 1; i++ ) { time = await this.WaitForAsyncTimer( time ); var lodIndex = i - 1; currentLODLevel = lodIndex; var lodEdgeLengthScale = 1f; if ( lodIndex >= 0 ) { lodEdgeLengthScale = lodLevels[ lodIndex ].lodEdgeLengthScale; } var lodMG = CreatePatchGeometry(); lodMG.lodEdgeLength = edgeLength * lodEdgeLengthScale; numTris += lodMG.vertices.Count; lodMG.ApplyTranslation( new Vector3( 0, 0, 0 ) ) ; mgs.Add( lodMG ); } X_numTriangles = numTris; LODLerpingData lerpingData = null; if ( lodLerpSteps > 0 ) { lerpingData = new LODLerpingData(); lerpingData.lowerLevelWeights = lowCurve; lerpingData.higherLevelWeights = highCurve; lerpingData.lerpSteps = lodLerpSteps; } output.Mesh = MeshGeometry.GenerateTrianglesLODMesh( mgs, lerpingData, false ); output.Mesh.SurfaceSetMaterial( 0, material ); } catch ( System.Exception e ) { this.LogError( e ); } currentLODLevel = cachedCurrentLODLevel; } } bool debugBlade = false; MeshGeometry CreatePatchGeometry() { var random = new LCG(); random.SetSeed( 1712 + seed ); var mg = new MeshGeometry(); var cellSizeX = ( patchSizeX + patchSize ) / ( bladesX + blades ); var cellSizeZ = ( patchSizeZ + patchSize ) / ( bladesZ + blades ); var allBladesX = bladesX + blades; var allBladesZ = bladesZ + blades; for ( int i = 0; i < allBladesX; i++ ) { var x = ( i + 0.5f ) * cellSizeX; for ( int j = 0; j < allBladesZ; j++ ) { var z = ( j + 0.5f ) * cellSizeZ; random.SetSeed( i * 11223 + j *12895 + seed ); var position = new Vector3( x, 0, z ); var yaw = random.Sample( yawRotation ); var rotationY = yaw * Mathf.Pi * 2f; var maxRotation = random.Sample( randomRotation ); var normalBlendingAmount = random.Sample( normalBlending ); var rotationOther = random.Next() * Mathf.DegToRad( maxRotation ); var rotationX = random.Next() * rotationOther; var rotationZ = rotationOther - rotationX; var positionOffset = Math3D.OnCircleXZ( random.Next() * Mathf.Pi * 2f ) * random.Sample( positionJitter ); positionOffset.X += random.Sample( positionJitterX ); positionOffset.Z += random.Sample( positionJitterZ ); var worldPosition = position + _patchOffset + positionOffset; var filterValue = Noise.PerlinXZ( Math3D.XYasXZ( filterScale ) * worldPosition + Math3D.XYasXZ( filterOffset ) + -GlobalPosition * Math3D.XYasXZ( positionToFilter )); var trsf = new Transform3D( new Basis( Math3D.RotateY( rotationY ) * Math3D.RotateX( rotationX ) * Math3D.RotateZ( rotationZ )), worldPosition ); debugBlade = X_numBlades < 10; var bladeMG = CreateBlade( random, worldPosition ); if ( bladeMG.numNormals == 0 || bladeMG.numUVs == 0 ) { this.LogInfo( "Invalid mg" ); } if ( filterValue > filterTreshold ) { continue; } var cL = Mathf.Clamp( currentLODLevel, -1, lodLevels.Length - 1 ); if ( cL != -1 ) { var lodFilterTreshold = lodLevels[ cL ].filter; if ( random.Next() >= lodFilterTreshold ) { continue; } } bladeMG.ApplyTransform( trsf ); MeshGeometry clonedBladeMG = null; if ( createBackFaces ) { clonedBladeMG = bladeMG.Clone(); } var yRange = bladeMG.GetRangeY(); // bladeMG.BlendNormals( normalBlendingDirection, normalBlendingAmount ); bladeMG.BlendNormalsOverY( normalBlendingDirection, normalBlendingAmount, yRange.min, yRange.max, normalBlendingAmountOverY ); mg.Add( bladeMG ); if ( createBackFaces ) { clonedBladeMG.FlipNormalDirection(); // clonedBladeMG.BlendNormals( normalBlendingDirection, normalBlendingAmount ); clonedBladeMG.BlendNormalsOverY( normalBlendingDirection, normalBlendingAmount, yRange.min, yRange.max, normalBlendingAmountOverY ); mg.Add( clonedBladeMG ); } X_numBlades ++; } } if ( mg.numNormals == 0 || mg.numUVs == 0 ) { this.LogInfo( "Invalid mg" ); } if ( centerPatch ) { var f = 100000; var h = centerPatchComputationHeightTreshold; var box = Box3.Create( new Vector3( -f, -f, -f ), new Vector3( f, h, f ) ); mg.CenterMesh( true, false, true, box ); } if ( vertexTurbulenceAmount != null ) { var turbulenceAmount = random.Sample( vertexTurbulenceAmount ); var turbulenceScale = Vector3.One * ( vertexTurbulenceScale == null ? 1 : random.Sample( vertexTurbulenceScale ) ); if ( currentLODLevel != -1 ) { turbulenceAmount *= lodLevels[ currentLODLevel ].turbulenceScale; } if ( vertexTurbulenceScaleX != null ) { turbulenceScale.X *= random.Sample( vertexTurbulenceScaleX ); } if ( vertexTurbulenceScaleY != null ) { turbulenceScale.Y *= random.Sample( vertexTurbulenceScaleY ); } if ( vertexTurbulenceScaleZ != null ) { turbulenceScale.Z *= random.Sample( vertexTurbulenceScaleX ); } mg.Turbulence( turbulenceAmount, turbulenceScale, Vector3.Zero ); } if ( xRemapper != null ) { mg.RemapX( xRemapper ); } if ( yRemapper != null ) { mg.RemapY( yRemapper ); } if ( zRemapper != null ) { mg.RemapZ( zRemapper ); } if ( scaleXZForY != null ) { mg.ScaleXZForY( scaleXZForY ); } if ( scaleXForY != null ) { mg.ScaleXForY( scaleXForY ); } if ( scaleZForY != null ) { mg.ScaleZForY( scaleZForY ); } return mg; } MeshGeometry CreateBlade( RandomEngine random, Vector3 position ) { // if ( debugBlade ) // { // this.LogInfo( "Blade:", X_numBlades ); // } var bladeSegments = this.bladeSegments; var currentLodLevel = Mathf.Clamp( currentLODLevel, -1, lodLevels.Length - 1 ); var bladeScaleMultiplier = 1f; var bladeWidthMultiplier = 1f; if ( currentLodLevel != -1 ) { bladeSegments = lodLevels[ currentLodLevel ].bladeSegments; bladeScaleMultiplier = lodLevels[ currentLodLevel ].bladeScaleMultiplier; bladeWidthMultiplier = lodLevels[ currentLodLevel].bladeWidthMultiplier; } var bmg = new MeshGeometry(); var inGround = random.Sample( bladeInGround ); var height = random.Sample( bladeHeight ); var size = height + inGround; var distancesToCenter = new Vector2( position.X / ( patchSizeX + patchSize ), position.Z / ( patchSizeZ + patchSize ) ); distancesToCenter = distancesToCenter.Abs(); var minDistance = distancesToCenter.Length(); var scaling = scaleByDistanceX.Sample( distancesToCenter.X ) * scaleByDistanceZ.Sample( distancesToCenter.Y ); scaling *= random.Sample( bladeScale ); scaling *= bladeScaleMultiplier; size *= scaling; var firstIsTriangle = false; var lastIsTriangle = false; var bladeWidthLerp = random.Next(); var bladeBendLerp = random.Next(); var uvMin = new Vector2( 0, 0 ); var uvMax = new Vector2( 1, 1 ); var uvSegments = uvSegmentRows * uvSegmentColumns; if ( uvSegments > 1 ) { var index = random.IntegerExclusive( uvSegments ); if ( uvSegmentWeightsClose != null ) { var weightsClose = MathX.GetCurveWeights( uvSegmentWeightsClose, uvSegments ); if ( uvSegmentWeightsFar != null ) { var weightsFar = MathX.GetCurveWeights( uvSegmentWeightsFar, uvSegments ); var distanceFade = minDistance; for ( int i = 0; i < uvSegments; i++ ) { weightsClose[ i ] += distanceFade * ( weightsFar[ i ] - weightsClose[ i ] ); } } index = random.IndexFromUnnormalizedWeights( weightsClose ); } var x = index % uvSegmentColumns; var y = Mathf.FloorToInt( index / uvSegmentColumns ); var xSize = 1f / uvSegmentColumns; var ySize = 1f / uvSegmentRows; uvMin.X = x * xSize; uvMin.Y = y * ySize; uvMax = uvMin + new Vector2( xSize, ySize ); } var endingTriangles = allowTrianglesOnEnds; if ( currentLodLevel != -1 ) { var useTris = lodLevels[ currentLodLevel ].allowTrianglesOnEnd; endingTriangles = TrilleanLogic.ToBool( useTris, endingTriangles ); } for ( int i = 0; i <= bladeSegments; i++ ) { var t = (float)i / bladeSegments; t = bladeSegmentMapping.Sample( t ); var v = 1f - t; var y = size * t - inGround; var bw2 = bladeWidth2 == null ? bladeWidth : bladeWidth2; var bb2 = bladeBending2 == null ? bladeBending : bladeBending2; var width = Mathf.Lerp( bladeWidth.Sample( v ), bw2.Sample( v ), bladeWidthLerp ); width *= scaling * bladeWidthMultiplier; // var bending = Mathf.Lerp( bladeBending.Sample( v ), bb2.Sample( v ), bladeBendLerp ) * Vector3.Forward.Z * scaling; // var bendingNormalAngle = Mathf.LerpAngle( MathX.CurveAngle( bladeBending, v ), MathX.CurveAngle( bb2, v ), bladeBendLerp ); // var bendingNormal = Math3D.NormalAngle( bendingNormalAngle ); var bending = 0; var bendingNormal = Vector3.Back; if ( endingTriangles && width < 0.005f && ( i == 0 || ( i == bladeSegments - 1 )) ) { bmg.vertices.Add( new Vector3( 0, y, bending ) ); bmg.normals.Add( bendingNormal ); bmg.uvs.Add( MapUV( new Vector2( 0.5f, v ), uvMin, uvMax ) ); if ( i == 0 ){ firstIsTriangle = true; } if ( i == bladeSegments - 1 ){ lastIsTriangle = true; } } else { bmg.vertices.Add( new Vector3( -width, y, bending ) ); bmg.vertices.Add( new Vector3( width, y, bending ) ); bmg.normals.Add( bendingNormal ); bmg.normals.Add( bendingNormal ); var uvCoord = width / _maxWidth; uvCoord = Mathf.Lerp( uvCoord, 1f, uvSegmentDistortion ); uvCoord *= uvSegmentMaxRange; var uvL = MapUV( new Vector2( -uvCoord + 0.5f, v ), uvMin, uvMax ); var uvR = MapUV( new Vector2( uvCoord + 0.5f, v ), uvMin, uvMax ); bmg.uvs.Add( uvL ); bmg.uvs.Add( uvR ); // if ( debugBlade ) // { // this.LogInfo( i, ">>", uvL, uvR ); // } } } for ( int i = 0; i < bladeSegments; i++ ) { var index = i * 2; if ( firstIsTriangle && index != 0 ) { index --; } if ( i == 0 && firstIsTriangle ) { bmg.AddTriangle( index + 0, index + 1, index + 2 ); } else if ( i == ( bladeSegments - 1 ) && lastIsTriangle ) { bmg.AddTriangle( index + 1, index + 0, index + 2 ); } else { bmg.AddQuad( index + 0, index + 1, index + 2, index + 3 ); } } var yStart = size * 0f - inGround; var yEnd = size * 1f - inGround; var startPosition = new Vector3( 0, yStart, 0 ); var endPosition = new Vector3( 0, yEnd, 0 ); var inputList = new List(){ startPosition, endPosition }; var inputSpline = new SplineCurveCreator().Create( inputList ); inputSpline.AutoOrientateByTangents( Vector3.Back ); var outputList = new List(); var numPoints = Mathf.Max( 2, bladeSegments * 2 ); for ( int i = 0; i < numPoints ; i++ ) { var t = (float)i / ( numPoints - 1f ); var y = size * t - inGround; var bb2 = bladeBending2 == null ? bladeBending : bladeBending2; var v = 1f - t; var bending = Mathf.Lerp( bladeBending.Sample( v ), bb2.Sample( v ), bladeBendLerp ) * Vector3.Forward.Z * scaling; var p = new Vector3( 0, y, bending ); outputList.Add( p ); } var outputSpline = new SplineCurveCreator().Create( outputList ); var outputPoints = outputSpline.points; if ( bladeTwisting != null ) { for ( int i = 0; i < outputPoints.Count; i++ ) { var t = (float)i / ( numPoints - 1f ); var v = 1f - t; var bt2 = bladeTwisting2 == null ? bladeTwisting : bladeTwisting2; var tw = Mathf.Lerp( bladeTwisting.Sample( v ), bt2.Sample( v ), bladeBendLerp ); var p = outputPoints[ i ]; p.twist = tw; } } outputSpline.AutoOrientateByTangents( Vector3.Back ); var rollingMix = random.Next(); var otherRolling = rolling2 == null ? rolling : rolling2; for ( int i = 0; i < outputPoints.Count - 1; i++ ) { var t = i / (float)( outputPoints.Count - 2 ); var rotation = rolling.Sample( t ); var rotation2 = otherRolling.Sample( t ); var mixedRotation = Mathf.Lerp( rotation, rotation2, rollingMix ); RollSpline( outputPoints, i, mixedRotation ); } var modifier = new SplinesDeformModifier(); modifier.sourceSplines = new SplineCurve[]{ inputSpline }; modifier.targetSplines = new SplineCurve[]{ outputSpline }; modifier.settings = new SplinesDeformerSettings(); bmg = modifier.Modify( bmg ); return bmg; } void RollSpline( List points, int start, float amount ) { var pivot = points[ start ].position; var rotation = Math3D.RotateX( MathX.DegreesToRadians * amount / (float) points.Count ).Normalized(); for ( int i = start + 1; i < points.Count; i++ ) { points[ i ].RotateAround( rotation, pivot ); } } Vector2 MapUV( Vector2 uv, Vector2 min, Vector2 max ) { uv = Math2D.Clamp01( uv ); return Math2D.Map( uv, Vector2.Zero, Vector2.One, min, max ); } } }