Disclaimer: I am NOT a strong math student so if my method is bad/partially wrong don’t be surprised. I am also not a strong writer so please excuse my spelling and grammar mistakes.
hi, today I’m going to be showing you how to create a hill with only parts and a little bit of math
But before we start, I think it’s important to understand the shape we are trying to create
The image below shows what a hill looks like which looks very similar to a bell curve (RIght).
You can use any bell curve function, but for this tutorial, I will be using the 2D Gaussian function with the following variables and function:
h = the height/amplitude of the the function (the peak)
s = the spread of the function (how wide the hill is before it flattens)
Step 1: Creating the grid
this shouldn’t be too hard, just a simple for loop for the x and z axis and each point will have a specific height calculated by the function
local gridSize = 40
local spacing = 4
local height = 25
local spread = 20
local thickness = 1
local location = Vector3.new(0, 0, 0)
for x = -gridSize / 2, gridSize / 2 do
for z = -gridSize / 2, gridSize / 2 do
local dx = x * spacing
local dz = z * spacing
-- this is just the 2D Gaussian function :)
local distanceSq = (dx * dx) + (dz * dz)
local y = height * math.exp(-distanceSq / (2 * (spread * spread)))
---
local pos = location + Vector3.new(dx, y, dz)
local part = Instance.new("Part")
part.Size = Vector3.new(spacing, thickness, spacing)
part.Position = pos
part.Anchored = true
part.Color = Color3.fromRGB(108, 255, 120)
part.Parent = workspace
end
end
this is what it should look like
Step 2: Orienting the parts
right now, it doesn’t look like a smooth hill at all. The heights and xz positions are correct, but the orientation of each part is wrong.
To fix this, I will be using the derivative for the xz axis to find the slope which will tell just how much it’s changing (Sorry if my hand writing is kinda bad lol).
--this comes from the derivative above
local function getSlopeByDerivative(x, z, y)
local slopeX = -y * x / (spread * spread)
local slopeZ = -y * z / (spread * spread)
return slopeX, slopeZ
end
local slopeX, slopeZ = getSlopeByDerivative(dx, dz, y)
--using the slopes, we find the normal of each part
local normal = Vector3.new(-slopeX, 1, -slopeZ).Unit
--this is only a temporary value, we will recalculated it after we find the forward vector
local right = Vector3.new(1, 0, 0)
local forward = right:Cross(normal).Unit
right = forward:Cross(normal).Unit
local cframe = CFrame.fromMatrix(pos, right, normal)
part.CFrame = cframe
finding the normal might be the most confusing part of this tutorial so i’ll explain it the best i can.
As we know, a normal is a vector that points away from the surface.
Example: Lets say slopeX = 3. This means for every 1 stud in the X axis, the part rises 3 studs.
So we can conclude the slope points in the direction the where it’s increasing, but the normal needs to point in the opposite direction so it faces away from the surface.
This is why we do Vector3.new(-slopeX, 1, -slopeZ).Unit and not Vector3.new(slopeX, 1, slopeZ).Unit
and if all is done correct we should have something like this:
Step 3: Fixing the gap
now the parts align correctly, but now we have another problem. There’s gaps in between each brick which makes it look bad, but why does this even happen?
Well, this all has to do with the distance between the neighboring part.
Example: Given 2 flat parts next to each other with positions (0,1,4) and (0,1,8) if you calculate the distance of these 2 parts, you should get 4. But say you changed the y value to… (0,1,4) and (0,2,8) now the distance is greater than 4 while the size stayed constant. This ultimately creates the gap you see in the image.
So how do we fix this? it’s pretty simple, you just have to find the magnitude between the slopeX and Z then add it so its original size
local slopeStrength = Vector3.new(slopeX, 0, slopeZ).Magnitude
local sizeMultiplier = 1 + slopeStrength * 0.5
part.Size = Vector3.new(spacing * sizeMultiplier, thickness, spacing * sizeMultiplier)
I should let you the 0.5 is arbitrary. It works well in when I tested it, but you can change it to what ever number you’d like.
And for the moment of truth…
it works (kinda)
On the side note, this solution is not fool proof as small spread values accompanied by big height values may look a little weird just letting you know.
Full script here:
local gridSize = 40
local spacing = 4
local height = 20
local spread = 30
local thickness = 1
local location = Vector3.new(0, 0, 0)
local function getSlopeByDerivative(x, z, y)
local slopeX = -y * x / (spread * spread)
local slopeZ = -y * z / (spread * spread)
return slopeX, slopeZ
end
-- we can't start at 0 otherwise the it won't be centered
for x = -gridSize / 2, gridSize / 2 do
for z = -gridSize / 2, gridSize / 2 do
local dx = x * spacing
local dz = z * spacing
-- this is just the 2D Gaussian function :)
local distanceSq = (dx * dx) + (dz * dz)
local y = height * math.exp(-distanceSq / (2 * (spread * spread)))
local pos = location + Vector3.new(dx, y, dz)
local slopeX, slopeZ = getSlopeByDerivative(dx, dz, y)
--using the slopes, we find the normal of each part
local normal = Vector3.new(-slopeX, 1, -slopeZ).Unit
--this is only a temporary value, we will recalculated it after we find the forward vector
local right = Vector3.new(1, 0, 0)
local forward = right:Cross(normal).Unit
right = forward:Cross(normal).Unit
local cframe = CFrame.fromMatrix(pos, right, normal)
local slopeStrength = Vector2.new(slopeX, slopeZ).Magnitude
local sizeMultiplier = 1 + slopeStrength * 0.5
local part = Instance.new("Part")
part.Size = Vector3.new(spacing * sizeMultiplier, thickness, spacing * sizeMultiplier)
part.CFrame = cframe
part.Anchored = true
part.Color = Color3.fromRGB(108, 255, 120)
part.Parent = workspace
end
end
- hell yeah man its awesome
- its ok ig, needs some work tbh
- it sucks
- just use blender bro









