# 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)