SkinnedGrass - Performant interactive foliage

Hi, I was recently messing around with a more performant approach to interactive foliage using skinned meshes

Download

Features


  • Mesh deformation around objects
  • Global and custom wind
  • Octrees + frame limit LOD for performance
  • Lots of customization options for different effects

Examples


How it works / performance


The module calculates collisions with an array of Vector3 positions/radiuses and uses octrees to find nearby bones and apply displacement. It’s a pretty simple effect, the main performance increase over other modules come from using skinned meshes and bone transforms instead of BulkMoveTo (about 3-4x faster when I tested). Reparenting a skinned mesh with x amount of bones is also more performant than reparenting x parts, which helps with moving grass in/out of the workspace when out of range

Transform performance test:

bulkmoveto on 2000 meshparts averages 3.426 ms

transform on 2000 bones in 20 skinned meshes (w/ same vertex count) averages .987 ms

Parenting performance test

reparenting 2000 meshparts - 18.526 ms

reparenting 20 skinned meshes with 2000 bones - < .5 ms

some other posts about transform performance:
https://devforum.roblox.com/t/how-would-i-optimize-my-entity-system-in-cpu/2620118/25
https://devforum.roblox.com/t/how-to-optimize-5000-parts-spinning/3032250/18

Downsides:

  • The biggest inconvenience with this approach is having to rig meshes outside of Roblox Studio. I included a template that emulates the native grass and a grass card template using quads and SurfaceAppearance that should cover some cases
  • I found that static meshparts are slightly more performant than static skinned meshes, the increased performance from skinned meshes is only really noticeable when moving lots of bones each frame
  • Shadows are also more expensive on skinned meshes

This module is more of an experimental effect and large amounts of grass will likely still be too resource-intensive for most devices. Feel free to modify and use it in your own projects, I’d love to see if you guys can get some use out of it lol

If you’re interested in other stuff I make it’ll be on my yt channel here

Basic Usage


Step 1 - Initialization

Require the module and initialize it using the .new() constructor
local SkinnedGrass = require(game.ReplicatedStorage.SkinnedGrass) -- or wherever the module is located
local grass = SkinnedGrass.new() --initialize the object

Step 2 - Add meshes

You can add new meshes to the simulation at runtime
local tableOfMeshes = {}
grass:AddMeshes(tableOfMeshes)

Step 3 - Running the effect

Use :Step() to update the simulation at any time. Use RenderStepped for smooth displacement every frame. The first parameter is delta time, the second parameter is a table of collisions, and the third parameter is an optional Vector3 to center the simulation around (by default centers around the player)
game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	local collisions = {character.PrimaryPart.Position}

	grass:Step(deltaTime, collisions) 
end)

Step 4 - Apply settings

Use :UpdateSettings() to update the effect at any time
local settingsTable = {CollisionAngle = 60}
grass:UpdateSettings(settingsTable)

Full list of settings:

local DEFAULT_SETTINGS = {
	RenderDistanceEnabled = true, -- If grass should be rendered in/out
	RenderDistance = 100, -- Distance at which grass is fully rendered out --150
	RenderInAnimation = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), -- TweenInfo to apply to bones rendering in. Set to nil for no animation (more performant)
	RenderOutAnimation = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.In), -- TweenInfo to apply to bones rendering out. Set to nil for no animation (more performant)
	RenderOutOffset = CFrame.new(0, -3, 0), -- Offset for bones when rendering in/out
	MeshStorage = nil, -- Folder to store meshes that are rendered out. By default creates a folder in ReplicatedStorage

	CollisionsEnabled = true, -- If grass should be simulated with collisions. To disable collisions, set this bool to false rather than setting collision distance to 0
	CollisionDistance = 100, -- Distance at which collisions are simulated
	CollisionRadiusInner = 4, -- Radius around collision where bones are fully displaced
	CollisionRadiusOuter = 6, -- Radius around collision where the displacement effect ends
	CollisionAngle = 90, -- Angle of the displacement effect
	CollisionTranslation = Vector3.new(0, 0, 0), -- Position offset of the displacement effect
	CollisionReturnDelay = 0, -- Time that the grass stays displaced before bouncing back (begins after collision stops)
	CollisionReturnInfo = TweenInfo.new(1.6, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out), -- TweenInfo for the return animation

	WindEnabled = true, -- If grass should be simulated with wind. To disable wind, set this to false rather than setting wind distance to 0
	WindDistanceInner = 10, -- Distance at which wind is simulated with max frame rate
	WindDistanceOuter = 50, -- Distance at which wind is simulated with min frame rate
	WindFramerateInner = 1, -- Frame rate for wind in inner range (1 = 100% frames, 0.5 = 50% frames, etc)
	WindFramerateOuter = 0.25, -- Frame rate for wind in outer range
	WindNoiseSize = 16, -- Size for wind noise calculations
	WindNoiseIntensity = 2, -- Intensity for wind noise calculations
	UseGlobalWind = true, -- When set to true, overrides WindDirection, WindSpeed, WindAngle and WindTranslation settings with Roblox's wind
	WindDirection = Vector3.new(1, 0, 0), -- Direction of the wind effect
	WindSpeed = 8, -- Speed of the wind effect
	WindAngleMin = 35, -- Min angle of the wind effect
	WindAngleMax = 45, -- Max angle of the wind effect
	WindTranslationMin = Vector3.new(0, 0, 0), -- Min position offset of the wind effect
	WindTranslationMax = Vector3.new(0, 0, 0), -- Max position offset of the wind effect
}
80 Likes

Hey, Ive actually made a similar module to this!

Honestly took me like 3 days to learn how to use bones and weight painting to make the grass, the way I did it was essentially just storing each grass blade position as a CFrame in an Octree (similar to you), and then updating those CFrame rotations using parallel luau (for sway), Maybe you could implement this too (if you havent already?)

7 Likes

Also dont mind the goofy swaying, I was experimenting :sweat_smile:

1 Like

This is way better than the time someone tried to sell grass for 5 dollars!

That aside, I am impressed by this, I never knew using bones was faster than :bulkMoveTo() I am downloading this right now. This is truly one of the best devforum resources out there.

4 Likes

hi, I used parallel luau in an earlier version but took it as it didn’t have a noticeable impact on performance (at least the way I did it), how did you structure the actors in your version?

1 Like

All I did was put the script that required it under an Actor, and use task.desynchronize() like this for the cframes:

task.desynchronize()
RunService.Heartbeat:Connect(function()
	local query = Octree.Tree:RadiusSearch(10)

	for _, v in query do
		v.cf *= CFrame.fromOrientation(math.sin(tick()) *	v.sway, 0, 0)
	end
end
task.synchronize()

--moving blades down here
...

This is done seperately to moving the blades, which is done in serial.

The CFrames are generated beforehand

4 Likes

Also, are you the same xander22 from Youtube? If you are, I’m athar_adv from the comments section!

2 Likes

yup! probably going to post this on there but idk

2 Likes

hi theres a small bug: if you die the glass doesnt move when you touch it anymore

2 Likes

Man i love creations like these. Its helps so much for people like me who want to maek cool games. Thank you for your contribution!

3 Likes

The fabled worst 5 dollar grass of roblox

6 Likes

that’s just the way I have the runner script set up, in your own scripts ideally you’d track character respawns and add the new positions to the collision table (I’ll change the runner script right now tho)

updated the model, should be fixed

How did you put the grass on the terrain? Did you place the grass individually or did you make a module to make the grass align with the terrain?

just ran a command that raycasts down and places bones, something like this

local skinnedMesh = nil -- your mesh
for _,d in skinnedMesh:GetDescendants() do
	if not d:IsA("Bone") then continue end
	local rotCFrame = d.WorldCFrame - d.WorldCFrame.Position
	local rayOrigin = d.WorldCFrame.Position + Vector3.new(0, 100, 0)
	local rayDirection = Vector3.new(0, -200, 0) -- Cast downward
	local result = workspace:Raycast(rayOrigin, rayDirection)
	if result then
		d.WorldCFrame = CFrame.new(result.Position) * rotCFrame
	end
end
3 Likes

Please add the ability to add a “shockwave” of some sorts so that explosions or blasts will make the nearby SkinnedGrass meshes to be influenced by them! :smiley:

4 Likes

you can already do this with the script

1 Like

Aw man, if i only knew this earlier…
I also tried to make my own grass, but i used bulkmoveto. But now that i know that, i will be able to do some crazy things with it.
Like for example my Particles Module. It would be way faster.

Also, can you show us a video of your interactive foliage but at a larger scale? I am really curious of the performance.

1 Like

you can fix this pretty easy by just changing the runner script to run on every time the player character is added or respawns

1 Like

how could i load/unload specific grass tiles from the module?

this is for a chunk loading grass system i was gonna use

1 Like