Skip to content

Instantly share code, notes, and snippets.

@Softwave
Last active October 20, 2023 07:54
Show Gist options
  • Save Softwave/d390272755a58a0714891517351e12ba to your computer and use it in GitHub Desktop.
Save Softwave/d390272755a58a0714891517351e12ba to your computer and use it in GitHub Desktop.
TerrainCreator
# IslandCreator: A tool for generating island terrain meshes
# procedurally with Simplex Noise
@tool
extends Node3D
# Globals
var _noise = FastNoiseLite.new()
@export var groundMaterial: Material
# Properties
@export var islandRadius:float = 100.0:
set(value):
islandRadius = value
generateTerrain()
@export var islandFallOffRate:float = 0.001:
set(value):
islandFallOffRate = value
generateTerrain()
@export var minimumFalloff:float = 3.0:
set(value):
minimumFalloff = value
generateTerrain()
@export var waterDepth:float = 2.0:
set(value):
waterDepth = value
generateTerrain()
@export var overlapDistance:float = 300.0:
set(value):
overlapDistance = value
generateTerrain()
# Just use as a makeshift "rebuild" button
@export var build: bool = false:
set(value):
build = value
generateTerrain()
@export var period:int = 90:
set(value):
period = value
generateTerrain()
@export var octaves:int = 6:
set(value):
octaves = value
generateTerrain()
@export var yScale:int = 20:
set(value):
yScale = value
generateTerrain()
# subWidth and subHeight (subdivisions) are used to determine the number of vertices in the mesh
@export var subWidth:int = 100:
set(value):
subWidth = value
generateTerrain()
@export var subHeight:int = 100:
set(value):
subHeight = value
generateTerrain()
# The terrain width and height are used to determine the size of the mesh
@export var terrainWidth:int = 400:
set(value):
terrainWidth = value
generateTerrain()
@export var terrainHeight:int = 400:
set(value):
terrainHeight = value
generateTerrain()
func generateTerrain() -> void:
# Remove the previous MeshInstance3D, if it exists
if get_child_count() > 0:
get_child(0).queue_free()
# Setup Simplex Noise
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
_noise.seed = randi()
_noise.fractal_octaves = octaves
_noise.frequency = 1.0 / period # Do we need this?
# Create new plane mesh
var planeMesh = PlaneMesh.new()
planeMesh.size = Vector2(terrainWidth, terrainHeight)
planeMesh.subdivide_depth = subWidth
planeMesh.subdivide_width = subHeight
# Setup SurfaceTool
var surfaceTool = SurfaceTool.new()
surfaceTool.create_from(planeMesh, 0)
var arrayPlane = surfaceTool.commit()
var dataTool = MeshDataTool.new()
dataTool.create_from_surface(arrayPlane, 0)
for i in range(dataTool.get_vertex_count()):
var vertex = dataTool.get_vertex(i)
# Get the distance from the center of the plane
var distanceFromCenter = vertex.distance_to(Vector3(0, 0, 0))
var fallOff = getFalloff(distanceFromCenter)
var noiseInfluence = getNoiseInfluence(distanceFromCenter)
# Apply falloff based on distance from center
vertex.y = (_noise.get_noise_3d(vertex.x, vertex.y, vertex.z) * yScale * noiseInfluence + fallOff * yScale)
dataTool.set_vertex(i, vertex)
for i in range(arrayPlane.get_surface_count()):
arrayPlane.clear_surfaces()
dataTool.commit_to_surface(arrayPlane)
# Generate with SurfaceTool
surfaceTool.begin(Mesh.PRIMITIVE_TRIANGLES)
surfaceTool.create_from(arrayPlane, 0)
surfaceTool.generate_normals()
# Create new MeshInstance3D
var meshInstance = MeshInstance3D.new()
meshInstance.mesh = surfaceTool.commit()
# If we're in the editor, add the mesh to the scene tree
if Engine.is_editor_hint():
self.add_child(meshInstance)
meshInstance.owner = self.get_tree().edited_scene_root
# Set the material
meshInstance.mesh.surface_set_material(0, groundMaterial)
func _ready() -> void:
# If we're in the editor, generate the terrain
if Engine.is_editor_hint():
generateTerrain()
func getFalloff(distanceFromCenter: float) -> float:
# If we're inside the island, return a falloff that keeps the terrain flat
if distanceFromCenter <= islandRadius:
return 0.0
# Start falloff at the edge of the islandRadius but gradually
if distanceFromCenter <= islandRadius + overlapDistance:
var t = (distanceFromCenter - islandRadius) / overlapDistance
return lerp(0.0, -minimumFalloff, t)
# For outside the overlap region, we do the same as before:
var maxDistance:float = Vector2(terrainWidth * 0.5, terrainHeight * 0.5).length()
var gradientDistance = maxDistance - islandRadius - overlapDistance
var t = (distanceFromCenter - islandRadius - overlapDistance) / gradientDistance
return lerpf(-minimumFalloff, -waterDepth, smoothstep(0.0, 1.0, t))
func smoothstep(edge0: float, edge1: float, x: float) -> float:
# Scale, and clamp x to 0..1 range
x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
# Evaluate polynomial
return x * x * (3 - 2 * x)
func getNoiseInfluence(distanceFromCenter:float) -> float:
if distanceFromCenter <= islandRadius:
return 1.0 # Full influence inside the island radius
# Begin reducing the noise influence only after islandRadius + overlapDistance
if distanceFromCenter <= islandRadius + overlapDistance:
var t = (distanceFromCenter - islandRadius) / overlapDistance
return 1.0 - t # Gradual reduction based on overlap distance
return 0.0 # No influence beyond the overlap distance
# TerrainCreator: A tool for generating terrain meshes procedurally with Simplex Noise
@tool
extends Node3D
# Globals
var _noise = FastNoiseLite.new()
@export var groundMaterial: Material
# Properties
@export var period:int = 50:
set(value):
period = value
generateTerrain()
@export var octaves:int = 6:
set(value):
octaves = value
generateTerrain()
@export var yScale:int = 20:
set(value):
yScale = value
generateTerrain()
# subWidth and subHeight (subdivisions) are used to determine the number of vertices in the mesh
@export var subWidth:int = 100:
set(value):
subWidth = value
generateTerrain()
@export var subHeight:int = 100:
set(value):
subHeight = value
generateTerrain()
# The terrain width and height are used to determine the size of the mesh
@export var terrainWidth:int = 200:
set(value):
terrainWidth = value
generateTerrain()
@export var terrainHeight:int = 200:
set(value):
terrainHeight = value
generateTerrain()
func generateTerrain() -> void:
# Remove the previous MeshInstance3D, if it exists
if get_child_count() > 0:
get_child(0).queue_free()
# Setup Simplex Noise
_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
_noise.seed = randi()
_noise.fractal_octaves = octaves
_noise.frequency = 1.0 / period # Do we need this?
# Create new plane mesh
var planeMesh = PlaneMesh.new()
planeMesh.size = Vector2(terrainWidth, terrainHeight)
planeMesh.subdivide_depth = subWidth
planeMesh.subdivide_width = subHeight
# Setup SurfaceTool
var surfaceTool = SurfaceTool.new()
surfaceTool.create_from(planeMesh, 0)
var arrayPlane = surfaceTool.commit()
var dataTool = MeshDataTool.new()
dataTool.create_from_surface(arrayPlane, 0)
for i in range(dataTool.get_vertex_count()):
var vertex = dataTool.get_vertex(i)
vertex.y = _noise.get_noise_3d(vertex.x, vertex.y, vertex.z) * yScale
dataTool.set_vertex(i, vertex)
for i in range(arrayPlane.get_surface_count()):
arrayPlane.clear_surfaces()
dataTool.commit_to_surface(arrayPlane)
# Generate with SurfaceTool
surfaceTool.begin(Mesh.PRIMITIVE_TRIANGLES)
surfaceTool.create_from(arrayPlane, 0)
surfaceTool.generate_normals()
# Create new MeshInstance3D
var meshInstance = MeshInstance3D.new()
meshInstance.mesh = surfaceTool.commit()
# If we're in the editor, add the mesh to the scene tree
if Engine.is_editor_hint():
self.add_child(meshInstance)
meshInstance.owner = self.get_tree().edited_scene_root
# Set the material
meshInstance.mesh.surface_set_material(0, groundMaterial)
func _ready() -> void:
# If we're in the editor, generate the terrain
if Engine.is_editor_hint():
generateTerrain()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment