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'
}