Procedural Grass on the Fly like in UDK

Procedural Grass on the Fly like in UDK

The code below in UDK generated procedural grass on the fly based on the players location. I’m sure this would be very useful to
a lot of people but sadly I don’t the skills to get it working in UE4. If anyone has the brain power to do it please help.

What it does in UDk landscape tool.

  • depending on player location check collision of sphere
  • trace down and find physx material
  • place mesh depending on physx material that was hit

it worked very well on landscape in UDK and hope someone can do a blueprint to do the same in UE4.


class NearFoliageManager extends Actor;

// Visible/ Collision Components
var const CylinderComponent	CylinderComponent;
var const DrawSphereComponent	SphereComponent;

// Grass Counts
var array<RuntimeGrass> currentGrass;
var int GrassToSpawnInRing;
var int offsetammount;
var PhysicalMaterial TerrainMatMask;

// Optimization
var Vector lastSpawnPosition;
var int ReCheckDistance;
var int MaxGrassCount;

simulated function PostBeginPlay() {
	super.PostBeginPlay();
	Owner.AttachComponent(SphereComponent);
	Owner.AttachComponent(CylinderComponent);
}

event Destroyed() {
	DestroyAllGrass();
	Owner.DetachComponent(SphereComponent);
	Owner.DetachComponent(CylinderComponent);
	super.Destroyed();
}

event Tick( float DeltaTime ) {
	local RuntimeGrass localGrass;

	super.Tick(DeltaTime);

	// Hacky replication check to ensure destroyed is called.
	// Also ensure this only exists on client that owns the pawn.
	if (NoReturnPawn(Owner).IsAliveAndWell() == false || NoReturnPawn(Owner).Controller == none) Destroy();


	// Only do both of these either on the client or in singleplayer (NO SPAWNED GRASS ON SERVER)
	if (WorldInfo.NetMode == NM_Client || WorldInfo.NetMode == NM_Standalone) {
		createGrassMesh(GrassToSpawnInRing);
	}

	if (WorldInfo.NetMode == NM_Client || WorldInfo.NetMode == NM_Standalone) {
		foreach currentGrass(localGrass) {
			localGrass.checkDistanceFromOwner();
		}
	}
}

function DestroyAllGrass() {
	local RuntimeGrass localGrass;

	foreach currentGrass(localGrass) {
		localGrass.Destroy();
	}
}

function RemoveGrassInstance(RuntimeGrass grassToRemove) {
	currentGrass.RemoveItem(grassToRemove);
}

// Creates a RunTimeGrass, if it succeeds the grass is placed into an array, otherwise it returns false.
unreliable client function createGrassMesh(int grassToSpawn) {
	local Vector spawnLocation;
	local Rotator spawnRotation;
	local RuntimeGrass spawnedGrass;
	local int i, rad;
	local float radian;

	// Trace Info
	local TraceHitInfo spawnedGrassTraceInfo;
	local Vector traceHitPos, traceHitNormal, traceEndPos;

	// Optimization :: Stops Spawning Grass if Max Reach
	if (currentGrass.Length > MaxGrassCount) return;

	// Optimization :: If player hasnt moved more than 100 UUs since last check, don't spawn grass.
	if (VSize(lastSpawnPosition - CylinderComponent.GetPosition()) < ReCheckDistance) return;

	rad = CylinderComponent.CollisionRadius-(offsetammount*2);
	lastSpawnPosition = CylinderComponent.GetPosition();

	for (i=0; i < grassToSpawn; i++) {

		// Creates the grass in a circle.
		radian = i * Pi/(grassToSpawn/ 2);
		spawnLocation = CylinderComponent.GetPosition();
        spawnLocation.X += rad * Cos(radian);
		spawnLocation.Y += rad * Sin(radian);

		// Offsets position to make it feel more random

		spawnLocation.X += (offsetammount * ((FRand()*2)-1));
		spawnLocation.Y += (offsetammount * ((FRand()*2)-1));

		spawnLocation.Z += 500; // Allows grass to spawn on ledges/ hills above player

		// Trace downwards to get surface/ hit location/ hitnormal
		traceEndPos = spawnLocation;
		traceEndPos.Z -= 1000;
		//DrawDebugLine(spawnLocation, traceEndPos, 128,128,255, true);
		Trace(traceHitPos, traceHitNormal,  traceEndPos, spawnLocation, false,, spawnedGrassTraceInfo);

		// If the physical material is not grass, back out.
		if(spawnedGrassTraceInfo.PhysMaterial != TerrainMatMask) goto'end';

		// Align grass to surface normal.
		spawnLocation = traceHitPos;
		spawnRotation = Rotator(normal(traceHitNormal));
		spawnRotation.Pitch-= 16384;

		// Spawn da grass. If it fails for whatever reason, back out.
		spawnedGrass = Spawn(class'RuntimeGrass', self,, spawnLocation,spawnRotation,,true);
		if (spawnedGrass != none) currentGrass.AddItem(spawnedGrass);
end:
	}
}

DefaultProperties
{

   Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
   bEnabled=TRUE
      bSynthesizeSHLight=TRUE
      bIsCharacterLightEnvironment=TRUE
      bUseBooleanEnvironmentShadowing=FALSE
   End Object
   Components.Add(MyLightEnvironment)
   LightEnvironment=MyLightEnvironment

	TerrainMatMask = PhysicalMaterial'BASIC_TESTING_1.physx_materials.TERRAIN_PHYSX_MAT'
//	physMatMask = MaterialInstanceConstant'BASIC_TESTING_1.Materials.ground_1_mat_INST'
//PhysicalMaterial'BASIC_TESTING_1.physx_materials.WOOD_PHYSX_MATERIAL'
	MaxGrassCount = 100
	ReCheckDistance = 100
	GrassToSpawnInRing = 25
	offsetammount = 64

	Begin Object Class=CylinderComponent NAME=CollisionCylinder LegacyClassName=Trigger_TriggerCylinderComponent_Class
		//CollideActors=true
		CollisionRadius=+3024
		CollisionHeight=+512.000000
		HiddenGame=true
        bBlockActors = false
	bCollideActors = false
	bStatic = false;
	bMovable = true;
	bNoDelete = false;
	bHidden = false;
	BlockRigidBody=false
	CastShadow=false
	bCastDynamicShadow=false
	//bUseOnePassLightingOnTranslucency=true
	LightingChannels=(Dynamic=true,Static=false)
	End Object
	CollisionComponent=CollisionCylinder
	CylinderComponent = CollisionCylinder
	Components.Add(CollisionCylinder)

/**
	Begin Object Class=DrawSphereComponent Name=Sphere
		bDrawOnlyIfSelected=false
		bDrawWireSphere=true
		SphereRadius=1024
		SphereSides=32
		SphereColor=(R=255,G=32,B=32)
		HiddenEditor=false
		HiddenGame=false
	End Object
	SphereComponent = Sphere
	Components.Add(Sphere)
**/

	CollisionType = COLLIDE_NoCollision


}



class RuntimeGrass extends StaticMeshActor
	notplaceable;

function checkDistanceFromOwner() {
	local CylinderComponent localManagerCollision;
	local float GrassDistance;

	if (Owner == none) return;

	localManagerCollision = NearFoliageManager(Owner).CylinderComponent;
	GrassDistance = VSize(Location - localManagerCollision.GetPosition());
	if (GrassDistance > localManagerCollision.CollisionRadius) {
		NearFoliageManager(Owner).RemoveGrassInstance(self  );
		//SetHidden(true);
		Destroy();
	}
}

event Tick( float DeltaTime ) {
	// If FoliageManager is destroyed whilst this is created some grass can be left over.
	// Hacky fix.
	if (Owner == none) Destroy();
}

DefaultProperties
{
       Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
   bEnabled=TRUE
      bSynthesizeSHLight=TRUE
      bIsCharacterLightEnvironment=TRUE
      bUseBooleanEnvironmentShadowing=FALSE
   End Object
   Components.Add(MyLightEnvironment)
   LightEnvironment=MyLightEnvironment

	Begin Object Name=StaticMeshComponent0
	//	StaticMesh=StaticMesh'MOVING_GRASS.grass_shapes.GRASSFLY_1_Pyramid001'
		//StaticMesh=StaticMesh'moving_grass.grass_shapes.NEW_GRASS_SHAPE_A'
	StaticMesh=StaticMesh'moving_grass.large_grass.HEDGE_GRASS_3L'
	End Object
	DrawScale = 1
	bNoDelete = false
	bStatic = false
	bMovable = true

		CastShadow=false
	bCastDynamicShadow=false
	//bUseOnePassLightingOnTranslucency=true
	LightingChannels=(Dynamic=true,Static=false)
}


The Below was in the player pawn class.


 // Spawn grass code needs to be in post begin play
if (myFoliageManager == none) myFoliageManager = Spawn(class'NearFoliageManager', self,,,,,true);


// SPAWN GRASS CODE
function UnPossessed() {
	myFoliageManager.Destroy();
	super.UnPossessed();

	//MaterialInstanceConstant'moving_grass.Materials.Procedural_Grass_MatINST'
	// parent is
	// Material'moving_grass.Materials.GRASS_CLUMP_LARGE_MAT2'
}

Hi Folks,

Using a trace from the players location Ive got some grass spawning in but not as advanced as the system above. The traces do find the
physx material on the landscape materials and do place meshes according to that Hit information they recover. Its not great at the moment
and needs a lot more work done to it but its a start. I very much doubt its efficient yet but it works which is the main thing.