Tutorial: How to create a smooth hill using parts

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
is this tutorial good?
  • hell yeah man its awesome
  • its ok ig, needs some work tbh
  • it sucks
  • just use blender bro :sob:
0 voters
12 Likes

For something similar I was thinking about using plane meshes (two triangles with 4 vertices) instead of parts.
User puts parts manually instead of a function, that could be boring (but that’s usually what builders do) but gives better control in terms of variation.
Vertex welding to be done using editable meshes in run-time.
That’s just a theory.

@KylrrH

See kids!! – This is why you need to go to school!

So you can do nerdy – I mean sick stuff like this!! XD!!

Personally – I wouldn’t of had the patience to do this in studio . . . with that being said – very well done man!

BLENDER!! I CHOOSE YOU!!! – USE MESH!!! – (CRITIAL HIT!!)

2 Likes

and yet you have voted for hell yeah man its awesome
image
i find it’s confusing, to say the least.

. . .

Sorry – forgot cant joke around here . . .

Ever heard of pokemon? . . .

2 Likes

is it sort of command line utility that monitors kind of production related processes?..

Im sorry . .. what?
I was simply just teasing, have no idea what your talking about to be fair.

me too.

“pokemon” looks like a name of command line utility an environment can have.
for example windows (windows is quite popular) have: sysmon64, procmon.exe, diskmon.exe, portmon.exe (have not tested, AI says they exist)
and i guess imaginary pokemon.exe fits here perfectly.
sorry about that.

2 Likes

If you are going to do procedural generation you should just create and edit plane with EditableMesh
It would be much easier just moving vertecies