Hi, I was recently messing around with a more performant approach to interactive foliage using skinned meshes
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() constructorlocal 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 runtimelocal 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 timelocal 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
}