# cube_sphere.gd
@tool
extends MeshInstance3D
@export_group("sphere")
@export_range(2, 256) var resolution: int = 16:
set(value):
resolution = value
if is_inside_tree():
_generate_surface()
@export_range(1, 12) var subdivisions: int = 1:
set(value):
subdivisions = value
if is_inside_tree():
_generate_surface()
@export_range (0.1, 100.0) var radius: float = 1.0:
set(value):
radius = value
if is_inside_tree():
_generate_surface()
@export var debug_mode = false:
set(value):
debug_mode = value
if is_inside_tree():
_generate_surface()
@export var wireframe_mode = false:
set(value):
wireframe_mode = value
_update_material()
@export var use_terrain_shader = false:
set(value):
use_terrain_shader = value
_update_material()
@export var use_world_shader = true:
set(value):
use_world_shader = value
_update_material()
@export var heightmap: Texture2D:
set(value):
heightmap = value
if is_inside_tree():
_update_material()
@export var earth_texture: Texture2D:
set(value):
earth_texture = value
if is_inside_tree():
_update_material()
@export_range(0.0, 0.2) var height_scale: float = 0.02:
set(value):
height_scale = value
if is_inside_tree():
_update_material()
@export var show_height_debug: bool = false:
set(value):
show_height_debug = value
if is_inside_tree():
_update_material()
func _ready():
# generate mesh on game start
_generate_surface()
_update_material()
func _generate_surface():
# if new mesh > create arraymesh for it
if not mesh:
mesh = ArrayMesh.new()
# start with clean slate
mesh.clear_surfaces()
# arrays for mesh construction
var verts = PackedVector3Array()
var uvs = PackedVector2Array()
var normals = PackedVector3Array()
var indices = PackedInt32Array()
var colors = PackedColorArray()
# mesh generation code : 6 faces of cube
var face_normals = [
Vector3.UP,
Vector3.DOWN,
Vector3.LEFT,
Vector3.RIGHT,
Vector3.FORWARD,
Vector3.BACK,
]
var face_colors = [
Color.CORAL,
Color.DARK_OLIVE_GREEN,
Color.AQUAMARINE,
Color.DODGER_BLUE,
Color.LIGHT_SALMON,
Color.LIGHT_GREEN,
]
var face_coverage_per_subface = 1.0 / subdivisions
# debug
if debug_mode:
print("\n--- mesh generation start ---")
print("resolution : ", resolution)
print("subdivisions : ", subdivisions)
print("radius : ", radius)
print("facecoveragepersubface : ", face_coverage_per_subface)
# generate 6 faces
for face_index in range(face_normals.size()):
var normal_dir = face_normals[face_index]
var current_face_color = face_colors[face_index]
if debug_mode:
print("\n--- face ", face_index,
" | normal : ", normal_dir,
" ---")
for y in range(subdivisions):
for x in range(subdivisions):
var start_uv = Vector2(x, y) * face_coverage_per_subface
var end_uv = start_uv + Vector2.ONE * face_coverage_per_subface
#breakpoint
if debug_mode and face_index == 0 and x == 0 and y == 0:
print(
" 1st subface - startuv : ", start_uv,
" | enduv : ", end_uv)
_create_face(
normal_dir,
start_uv,
end_uv,
verts,
normals,
uvs,
indices,
colors,
current_face_color,
#face_index == 0 and x == 0 and y == 0 # debug 1st subface only
)
# assemble final mesh data
var surface_array = []
surface_array.resize(Mesh.ARRAY_MAX)
# assign arrays to surface array
surface_array[Mesh.ARRAY_VERTEX] = verts
surface_array[Mesh.ARRAY_TEX_UV] = uvs
surface_array[Mesh.ARRAY_NORMAL] = normals
surface_array[Mesh.ARRAY_INDEX] = indices
surface_array[Mesh.ARRAY_COLOR] = colors
# add surface to mesh
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, surface_array)
if debug_mode:
print("\n--- mesh generation done ---")
print("totalvertices : ", verts.size())
print("totalindices : ", indices.size())
_update_material()
# save mesh to .tres file to be reused (need be not re-generated, like
# global resource ? ie world globe map as game ui entry into game levels /
# individual world countries ...
ResourceSaver.save(mesh, "res://sphere.tres", ResourceSaver.FLAG_COMPRESS)
func _update_material():
if not mesh or mesh.get_surface_count() == 0:
return
var shader_mat = ShaderMaterial.new()
if wireframe_mode:
# wireframe material
shader_mat.shader = load("res://shaders/wireframe.gdshader")
elif use_world_shader:
# world shader with heightmap and earth texture
shader_mat.shader = load("res://shaders/world.gdshader")
if heightmap:
shader_mat.set_shader_parameter("height_map", heightmap)
if earth_texture:
shader_mat.set_shader_parameter("earth_texture", earth_texture)
shader_mat.set_shader_parameter("height_scale", height_scale)
shader_mat.set_shader_parameter("apply_height", true)
shader_mat.set_shader_parameter("show_height_debug", show_height_debug)
elif use_terrain_shader:
# cartoon shader : color per height
shader_mat.shader = load("res://shaders/terrain.gdshader")
if heightmap:
shader_mat.set_shader_parameter("height_map", heightmap)
shader_mat.set_shader_parameter("height_scale", height_scale)
shader_mat.set_shader_parameter("apply_height", true)
shader_mat.set_shader_parameter("show_height_debug", show_height_debug)
else:
# default: color faces
shader_mat.shader = load("res://shaders/color_faces.gdshader")
set_surface_override_material(0, shader_mat)
func _create_face(
p_normal: Vector3,
p_start_uv: Vector2,
p_end_uv: Vector2,
p_verts: PackedVector3Array,
p_normals: PackedVector3Array,
p_uvs: PackedVector2Array,
p_indices: PackedInt32Array,
p_colors: PackedColorArray,
p_face_color: Color,
):
# create 2 axes perpendicular to face normal
var axis_a = Vector3(p_normal.y, p_normal.z, p_normal.x)
var axis_b = p_normal.cross(axis_a)
if debug_mode:
print("\n createface called")
print("\n normal : ", p_normal)
print("\n axisa : ", axis_a)
print("\n axisb : ", axis_b)
print("\n startuv : ", p_start_uv)
print("\n enduv : ", p_end_uv)
# get number of vertices we have
var base_vertex_index = p_verts.size()
# calculate step sizes
var dx = (p_end_uv.x - p_start_uv.x) / (resolution - 1)
var dy = (p_end_uv.y - p_start_uv.y) / (resolution - 1)
# breakpoint
if debug_mode:
print(" dx : ", dx, " dy : ", dy)
print(" basevertexindex : ", base_vertex_index)
# start position
var ty = p_start_uv.y
# loop through grid of vertices
for y in range(resolution):
var tx = p_start_uv.x
for x in range(resolution):
var i = x + y * resolution
#breakpoint
# map cube point onto a unit sphere
var point_on_unit_cube = p_normal \
+ (tx - 0.5) * 2.0 * axis_a \
+ (ty - 0.5) * 2.0 * axis_b
# map to sphere
var point_on_unit_sphere = _cube_point_to_sphere_point(
point_on_unit_cube)
# final vertex position
var final_vertex = point_on_unit_sphere * radius
#breakpoint
# debug 1st 4 corners & middle, prevent known result warning
@warning_ignore("integer_division")
var mid_point := resolution / 2
if debug_mode and (
(x == 0 and y == 0) or # top-left
(x == resolution - 1 and y ==0) or # top-right
(x == 0 and y == resolution - 1) or # bottom-left
(x == resolution - 1 and y == resolution - 1) or # bottom-right
(x == mid_point and y == mid_point) # middle
):
print("\n vertex ", x, "-", y, " i=", i)
print("\n tx : ", tx, " ty : ", ty)
print("\n pointoncube : ", point_on_unit_cube)
print("\n pointonsphere : ", point_on_unit_sphere)
print("\n finalvertex : ", final_vertex)
# add vertex and normal
p_verts.push_back(final_vertex)
#p_verts.push_back(point_on_unit_sphere * radius)
p_normals.push_back(point_on_unit_sphere)
p_colors.push_back(p_face_color)
# uv-based on grid position
#var uv = Vector2(
#float(x) / (resolution - 1.0),
#float(y) / (resolution - 1.0)
#)
var uv = GeoMaths.point_to_uv(point_on_unit_cube)
# image origin is top-left : flip vertical & horizontal
#uv.x = 1.0 - uv.x
#uv.y = 1.0 - uv.y
# wrap horizontal to prevent image seam todo not working
#uv.x = uv.x - floor(uv.x)
p_uvs.push_back(uv)
# create 2 tris for quad
if x != resolution - 1 and y != resolution - 1:
# 1st tris
p_indices.push_back(base_vertex_index + i)
p_indices.push_back(base_vertex_index + i + resolution)
p_indices.push_back(base_vertex_index + i + resolution + 1)
# 2nd tris
p_indices.push_back(base_vertex_index + i)
p_indices.push_back(base_vertex_index + i + resolution + 1)
p_indices.push_back(base_vertex_index + i + 1)
tx += dx
ty += dy
## maps a point on a unit cube to a point on a unit sphere
# http://mathproofs.blogspot.com/2005/ 07/mapping-cube-to-sphere.html
func _cube_point_to_sphere_point(p: Vector3) -> Vector3:
var x2 = p.x * p.x / 2.0
var y2 = p.y * p.y / 2.0
var z2 = p.z * p.z / 2.0
var x = p.x * sqrt(1.0 - y2 - z2 + (p.y * p.y * p.z * p.z) / 3.0)
var y = p.y * sqrt(1.0 - z2 - x2 + (p.x * p.x * p.z * p.z) / 3.0)
var z = p.z * sqrt(1.0 - x2 - y2 + (p.x * p.x * p.y * p.y) / 3.0)
return Vector3(x, y, z)