└── addons └── level_block ├── CHANGELOG.md ├── LICENSE ├── README.md ├── clear.svg ├── clear.svg.import ├── default_material.tres ├── example_tileset.png ├── example_tileset.png.import ├── icon.png ├── icon.png.import ├── icon.svg ├── icon.svg.import ├── level_block.gd ├── level_block_gizmo.gd ├── level_block_inspector.gd ├── level_block_node.gd ├── plugin.cfg ├── src └── navmesh_parser.gd ├── texture_selector.gd └── texture_selector_scene.tscn /addons/level_block/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.0.1] - 2025-02-08 9 | 10 | **Note:** This release requires `LevelBlock` nodes to be inside `NavigationRegion3D` (or configured with node groups in `NavigationMesh` resource) to be considered for navigation mesh baking. Previous behaviour of navmeshes generating outside `NavigationRegion3D` was incorrect. 11 | 12 | ### Addded 13 | 14 | - Support navigation mesh baking AABB filter 15 | 16 | ### Fixed 17 | 18 | - Improve navigation mesh baking performance 19 | 20 | ## [3.0.0] - 2024-12-30 21 | 22 | ### Changed 23 | 24 | - **Breaking:** Requires Godot 4.3 25 | 26 | ### Added 27 | 28 | - Support baking navigation meshes ([#15](https://github.com/ReunMedia/godot-levelblock/pull/15)) 29 | 30 | ## [2.0.0] - 2023-06-07 - Godot 4 support 31 | 32 | **🎉 LevelBlock has been updated to support Godot 4! 🎉** 33 | 34 | Check out the [godot-3 branch](https://github.com/ReunMedia/godot-levelblock/tree/godot-3) for 1.x version that supports Godot 3.5. 35 | 36 | Further development efforts will primarily be focused on the new version and we can't guarantee new features for Godot 3.5, but if you encounter any bugs feel free to [submit an issue](https://github.com/ReunMedia/godot-levelblock/issues) or [open a pull request](https://github.com/ReunMedia/godot-levelblock/pulls). 37 | 38 | ### Added 39 | 40 | - Example tileset is bundled with the plugin 41 | 42 | ### Changed 43 | 44 | - Updated plugin to support Godot 4 45 | - Smaller Inspector texture preview 46 | 47 | ## [1.0.2] - 2022-12-22 48 | 49 | ### Fixed 50 | 51 | - Global transforms are only set inside scene tree 52 | - Physics bodies now have object instance attached (enables getting collider via raycasts) 53 | 54 | ## [1.0.1] - 2022-12-17 55 | 56 | ### Fixed 57 | 58 | - Fixed node visibility not being applied 59 | - Fixed global transformation not being applied 60 | 61 | ## [1.0.0] - 2022-12-15 62 | 63 | - Initial release 64 | 65 | [3.0.1]: https://github.com/ReunMedia/godot-levelblock/compare/3.0.0...3.0.1 66 | [3.0.0]: https://github.com/ReunMedia/godot-levelblock/compare/2.0.0...3.0.0 67 | [2.0.0]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.2...2.0.0 68 | [1.0.2]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.1...1.0.2 69 | [1.0.1]: https://github.com/ReunMedia/godot-levelblock/compare/1.0.0...1.0.1 70 | [1.0.0]: https://github.com/ReunMedia/godot-levelblock/releases/tag/1.0.0 71 | -------------------------------------------------------------------------------- /addons/level_block/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Reun Media Partnership 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/level_block/README.md: -------------------------------------------------------------------------------- 1 | # LevelBlock plugin for Godot 4 2 | ![screenshot](https://github.com/ReunMedia/godot-levelblock/assets/37181529/94a5e1c3-e041-46d4-9961-d3cd526bf07a) 3 | 4 | ![Godot - v4.0](https://img.shields.io/badge/Godot-v4.0-478cbf?style=flat-square&logo=godot-engine&labelColor=25282b) 5 | [![Asset Library](https://img.shields.io/badge/Asset_Library-2ea44f?style=flat-square)](https://godotengine.org/asset-library/asset/1924) 6 | 7 | _[Click here for Godot 3.5 version](https://github.com/ReunMedia/godot-levelblock/tree/godot-3)_ 8 | 9 | **LevelBlock** is a new node for Godot 4 meant for the creation of levels in dungeon crawler -style games. 10 | 11 | This node acts as an inside-facing cube, using a texture atlas sheet to display different parts for each face. 12 | ## Getting started 13 | 1. Download the plugin from GitHub or [Godot Asset Library](https://godotengine.org/asset-library/asset/1924) and install it. See a guide here: [Godot Docs](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/installing_plugins.html) 14 | 2. Add new LevelBlock nodes to your scene. 15 | 3. Configure the texture sheet (and optionally material) you want to use. 16 | 4. Configure the size of the square textures contained in the texture sheet. 17 | 5. Customize each face of the LevelBlock using the face values. Negative values disable generating the face. 18 | 6. Optionally enable generating collisions, generating occlusion culling and flipping the faces. 19 | ## Features 20 | - Use a single texture sheet, with each face displaying a different part of it using an index value. This texture sheet will replace the albedo texture of the material. 21 | - Customize the material or use the included default material. 22 | - Use an arbitary size for the square textures in the atlas. 23 | - Quickly customize the texture displayed on each face. 24 | - Automatic collision generation for visible faces. 25 | - Automatic occluder node generation for visible faces. 26 | - Option to flip faces to face outward instead. 27 | - Uses Godot's server system for optimized results. 28 | ## Limitations 29 | - Only square textures supported. 30 | - All textures in the atlas must be the same size. 31 | - Works best if texture filtering is disabled for a pixelated look. 32 | - No support for normal or roughness maps, only albedo. 33 | -------------------------------------------------------------------------------- /addons/level_block/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /addons/level_block/clear.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://da5akf336dc8" 6 | path="res://.godot/imported/clear.svg-029a2888fe635d68609c343a20d9a19a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/level_block/clear.svg" 14 | dest_files=["res://.godot/imported/clear.svg-029a2888fe635d68609c343a20d9a19a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/level_block/default_material.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://c71yladfjhgud"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://dul4gcwu55m0b" path="res://addons/level_block/example_tileset.png" id="1_s3lkm"] 4 | 5 | [resource] 6 | specular_mode = 2 7 | albedo_texture = ExtResource("1_s3lkm") 8 | metallic_specular = 0.0 9 | texture_filter = 0 10 | -------------------------------------------------------------------------------- /addons/level_block/example_tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReunMedia/godot-levelblock/46676e37b9d95026d45be68475e438d18147b092/addons/level_block/example_tileset.png -------------------------------------------------------------------------------- /addons/level_block/example_tileset.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dul4gcwu55m0b" 6 | path="res://.godot/imported/example_tileset.png-9c705cf817a28145047f8b10da2ebb52.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/level_block/example_tileset.png" 14 | dest_files=["res://.godot/imported/example_tileset.png-9c705cf817a28145047f8b10da2ebb52.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=0 35 | -------------------------------------------------------------------------------- /addons/level_block/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReunMedia/godot-levelblock/46676e37b9d95026d45be68475e438d18147b092/addons/level_block/icon.png -------------------------------------------------------------------------------- /addons/level_block/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="keep" 4 | -------------------------------------------------------------------------------- /addons/level_block/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /addons/level_block/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d14dv7fcrlyxm" 6 | path="res://.godot/imported/icon.svg-37502c4328f880b221342b460c8ed07a.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/level_block/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-37502c4328f880b221342b460c8ed07a.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/level_block/level_block.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const Icon = preload("res://addons/level_block/icon.svg") 5 | const BlockNode = preload("res://addons/level_block/level_block_node.gd") 6 | const GizmoPlugin = preload("res://addons/level_block/level_block_gizmo.gd") 7 | const InspectorPlugin = preload("res://addons/level_block/level_block_inspector.gd") 8 | const NavmeshParser = preload("res://addons/level_block/src/navmesh_parser.gd") 9 | 10 | var gizmo_plugin = GizmoPlugin.new() 11 | var inspector_plugin = InspectorPlugin.new() 12 | var navmesh_parser = NavmeshParser.new() 13 | 14 | func _enter_tree(): 15 | add_custom_type("LevelBlock", "Node3D", BlockNode, Icon) 16 | add_node_3d_gizmo_plugin(gizmo_plugin) 17 | add_inspector_plugin(inspector_plugin) 18 | 19 | navmesh_parser.create_parser() 20 | 21 | func _exit_tree(): 22 | remove_custom_type("LevelBlock") 23 | remove_node_3d_gizmo_plugin(gizmo_plugin) 24 | remove_inspector_plugin(inspector_plugin) 25 | 26 | navmesh_parser.delete_parser() 27 | -------------------------------------------------------------------------------- /addons/level_block/level_block_gizmo.gd: -------------------------------------------------------------------------------- 1 | extends EditorNode3DGizmoPlugin 2 | 3 | 4 | const LevelBlock := preload("res://addons/level_block/level_block_node.gd") 5 | 6 | const cube_line_points = [ 7 | Vector3(-1.0, -1.0, -1.0), 8 | Vector3(1.0, -1.0, -1.0), 9 | Vector3(1.0, -1.0, -1.0), 10 | Vector3(1.0, -1.0, 1.0), 11 | Vector3(1.0, -1.0, 1.0), 12 | Vector3(-1.0, -1.0, 1.0), 13 | Vector3(-1.0, -1.0, 1.0), 14 | Vector3(-1.0, -1.0, -1.0), 15 | Vector3(-1.0, 1.0, -1.0), 16 | Vector3(1.0, 1.0, -1.0), 17 | Vector3(1.0, 1.0, -1.0), 18 | Vector3(1.0, 1.0, 1.0), 19 | Vector3(1.0, 1.0, 1.0), 20 | Vector3(-1.0, 1.0, 1.0), 21 | Vector3(-1.0, 1.0, 1.0), 22 | Vector3(-1.0, 1.0, -1.0), 23 | Vector3(-1.0, -1.0, -1.0), 24 | Vector3(-1.0, 1.0, -1.0), 25 | Vector3(1.0, -1.0, -1.0), 26 | Vector3(1.0, 1.0, -1.0), 27 | Vector3(1.0, -1.0, 1.0), 28 | Vector3(1.0, 1.0, 1.0), 29 | Vector3(-1.0, -1.0, 1.0), 30 | Vector3(-1.0, 1.0, 1.0), 31 | ] 32 | 33 | func _get_gizmo_name(): 34 | return "LevelBlock" 35 | 36 | func _init(): 37 | create_material("Cube", Color.ORANGE) 38 | 39 | func _has_gizmo(node): 40 | return node is LevelBlock 41 | 42 | func _redraw(gizmo): 43 | gizmo.clear() 44 | 45 | var block = gizmo.get_node_3d() as LevelBlock 46 | var cube := BoxMesh.new() 47 | cube.size = Vector3.ONE * 2.0 48 | gizmo.add_collision_triangles(cube.generate_triangle_mesh()) 49 | var lines = PackedVector3Array(cube_line_points) 50 | gizmo.add_lines(lines, get_material("Cube", gizmo)) 51 | -------------------------------------------------------------------------------- /addons/level_block/level_block_inspector.gd: -------------------------------------------------------------------------------- 1 | extends EditorInspectorPlugin 2 | 3 | const BlockNode := preload("res://addons/level_block/level_block_node.gd") 4 | const TextureSelector := preload("res://addons/level_block/texture_selector.gd") 5 | 6 | var face_paths := [ 7 | "north_face", 8 | "east_face", 9 | "south_face", 10 | "west_face", 11 | "top_face", 12 | "bottom_face" 13 | ] 14 | 15 | 16 | func _can_handle(object): 17 | if object is BlockNode: 18 | return true 19 | return false 20 | 21 | func _parse_property(object, type, path, hint, hint_text, usage, wide): 22 | if type == TYPE_INT: 23 | for p in face_paths: 24 | if p == path: 25 | var selector := TextureSelector.new() 26 | selector.texture_sheet = object.texture_sheet 27 | object.connect("texture_updated", Callable(selector, "update_texture")) 28 | object.connect("texture_size_updated", Callable(selector, "update_texture_size")) 29 | selector.texture_size = object.texture_size 30 | add_property_editor(path, selector) 31 | return true 32 | return false 33 | else: 34 | return false 35 | -------------------------------------------------------------------------------- /addons/level_block/level_block_node.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node3D 3 | 4 | signal texture_updated(new_texture) 5 | signal texture_size_updated(new_size) 6 | 7 | const size = 1.0 8 | 9 | @export var material:BaseMaterial3D = load("res://addons/level_block/default_material.tres") 10 | @export var texture_sheet:Texture2D = null : set = set_texture 11 | func set_texture(new_value): 12 | texture_sheet = new_value 13 | emit_signal("texture_updated", texture_sheet) 14 | refresh() 15 | @export var texture_size:float = 32 : set = set_texture_size 16 | func set_texture_size(new_value): 17 | texture_size = new_value 18 | emit_signal("texture_size_updated", texture_size) 19 | refresh() 20 | @export var north_face:int = -1 : set = set_north 21 | func set_north(new_value): 22 | north_face = new_value 23 | refresh() 24 | @export var east_face:int = -1 : set = set_east 25 | func set_east(new_value): 26 | east_face = new_value 27 | refresh() 28 | @export var south_face:int = -1 : set = set_south 29 | func set_south(new_value): 30 | south_face = new_value 31 | refresh() 32 | @export var west_face:int = -1 : set = set_west 33 | func set_west(new_value): 34 | west_face = new_value 35 | refresh() 36 | @export var top_face:int = -1 : set = set_top 37 | func set_top(new_value): 38 | top_face = new_value 39 | refresh() 40 | @export var bottom_face:int = -1 : set = set_bottom 41 | func set_bottom(new_value): 42 | bottom_face = new_value 43 | refresh() 44 | @export var flip_faces:bool = false : set = set_flip_faces 45 | func set_flip_faces(new_value): 46 | flip_faces = new_value 47 | refresh() 48 | @export var generate_collision:bool = true : set = set_generate_collision 49 | func set_generate_collision(new_value): 50 | generate_collision = new_value 51 | refresh() 52 | @export var generate_occluders:bool = false : set = set_generate_occluders 53 | func set_generate_occluders(new_value): 54 | generate_occluders = new_value 55 | refresh() 56 | 57 | var faces 58 | var visual 59 | var body 60 | var mesh 61 | var shape 62 | var occluders : Array 63 | ## Stores mesh faces for navmesh generation 64 | var mesh_faces := PackedVector3Array() 65 | ## Stores mesh AABB for navmesh generation 66 | var mesh_aabb := AABB() 67 | 68 | func _ready(): 69 | set_notify_transform(true) 70 | refresh() 71 | 72 | func refresh(): 73 | clear() 74 | faces = [north_face, east_face, south_face, west_face, top_face, bottom_face] 75 | if faces.max() < 0: 76 | return 77 | if generate_occluders: 78 | create_occluders() 79 | 80 | mesh = create_mesh() 81 | 82 | # Store mesh faces and AABB for navmesh generation 83 | mesh_faces = mesh.get_faces() 84 | mesh_aabb = mesh.get_aabb() 85 | 86 | mesh.surface_set_material(0, material) 87 | material.albedo_texture = texture_sheet 88 | # RenderingServer 89 | visual = RenderingServer.instance_create() 90 | RenderingServer.instance_set_base(visual, mesh) 91 | if is_inside_tree(): 92 | RenderingServer.instance_set_scenario(visual, get_world_3d().scenario) 93 | RenderingServer.instance_set_transform(visual, global_transform) 94 | # PhysicsServer3D 95 | if !generate_collision: 96 | return 97 | body = PhysicsServer3D.body_create() 98 | PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_STATIC) 99 | shape = PhysicsServer3D.concave_polygon_shape_create() 100 | PhysicsServer3D.shape_set_data(shape, {"faces" : mesh.get_faces()}) 101 | if is_inside_tree(): 102 | PhysicsServer3D.body_add_shape(body, shape, global_transform) 103 | PhysicsServer3D.body_set_space(body, get_world_3d().space) 104 | PhysicsServer3D.body_set_ray_pickable(body, true) 105 | PhysicsServer3D.body_attach_object_instance_id(body, get_instance_id()) 106 | 107 | func clear(): 108 | if visual is RID: 109 | RenderingServer.free_rid(visual) 110 | visual = null 111 | if body is RID: 112 | PhysicsServer3D.free_rid(body) 113 | body = null 114 | if shape is RID: 115 | PhysicsServer3D.free_rid(shape) 116 | shape = null 117 | if occluders.size() > 0: 118 | for o in occluders: 119 | o.queue_free() 120 | occluders.clear() 121 | mesh_faces.clear() 122 | 123 | func get_uv_gap() -> float: 124 | return float(texture_size) / texture_sheet.get_size().x 125 | 126 | func get_uv_position(index: int) -> Vector2: 127 | var pos = Vector2.ZERO 128 | pos.x = fmod(get_uv_gap() * index, 1.0) 129 | pos.y = floor(index / (1.0 / get_uv_gap())) * get_uv_gap() 130 | return pos 131 | 132 | func create_mesh() -> Mesh: 133 | var normals = [Vector3.BACK, Vector3.LEFT, Vector3.FORWARD, Vector3.RIGHT, Vector3.DOWN, Vector3.UP] 134 | var rot_axis = [Vector3.DOWN, Vector3.LEFT] 135 | if flip_faces: 136 | normals = [Vector3.FORWARD, Vector3.RIGHT, Vector3.BACK, Vector3.LEFT, Vector3.UP, Vector3.DOWN] 137 | var rot_angle = [0.0, PI / 2.0, PI, PI + (PI / 2), -(PI / 2.0), PI / 2.0] 138 | 139 | var st = SurfaceTool.new() 140 | st.begin(Mesh.PRIMITIVE_TRIANGLES) 141 | for i in range(6): 142 | st.set_normal(normals[i]) 143 | st.set_uv(get_uv_position(faces[i])) 144 | var vertex_0 := Vector3(-size, size, -size) 145 | vertex_0 = vertex_0.rotated(rot_axis[i / 4], rot_angle[i]) 146 | st.add_vertex(vertex_0) 147 | 148 | st.set_normal(normals[i]) 149 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(1.0, 0.0))) 150 | var vertex_1 := Vector3(size, size, -size) 151 | vertex_1 = vertex_1.rotated(rot_axis[i / 4], rot_angle[i]) 152 | st.add_vertex(vertex_1) 153 | 154 | st.set_normal(normals[i]) 155 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(1.0, 1.0))) 156 | var vertex_2 := Vector3(size, -size, -size) 157 | vertex_2 = vertex_2.rotated(rot_axis[i / 4], rot_angle[i]) 158 | st.add_vertex(vertex_2) 159 | 160 | st.set_normal(normals[i]) 161 | st.set_uv(get_uv_position(faces[i]) + (get_uv_gap() * Vector2(0.0, 1.0))) 162 | var vertex_3 := Vector3(-size, -size, -size) 163 | vertex_3 = vertex_3.rotated(rot_axis[i / 4], rot_angle[i]) 164 | st.add_vertex(vertex_3) 165 | 166 | if faces[i] < 0: 167 | continue 168 | 169 | if flip_faces: 170 | st.add_index(3 + (i * 4)) 171 | st.add_index(1 + (i * 4)) 172 | st.add_index(0 + (i * 4)) 173 | 174 | st.add_index(3 + (i * 4)) 175 | st.add_index(2 + (i * 4)) 176 | st.add_index(1 + (i * 4)) 177 | continue 178 | 179 | st.add_index(0 + (i * 4)) 180 | st.add_index(1 + (i * 4)) 181 | st.add_index(3 + (i * 4)) 182 | 183 | st.add_index(1 + (i * 4)) 184 | st.add_index(2 + (i * 4)) 185 | st.add_index(3 + (i * 4)) 186 | 187 | return st.commit() 188 | 189 | func create_occluders(): 190 | var positions = [Vector3.FORWARD, Vector3.RIGHT, Vector3.BACK, Vector3.LEFT, Vector3.UP, Vector3.DOWN] 191 | var rot_axis = [Vector3.UP, Vector3.RIGHT] 192 | var rot_angle = [0.0, -(PI / 2.0), PI, PI / 2, PI / 2.0, -(PI / 2.0)] 193 | for i in range(6): 194 | if faces[i] < 0: 195 | continue 196 | var occluder = OccluderInstance3D.new() 197 | occluder.occluder = QuadOccluder3D.new() 198 | occluder.occluder.size *= 2.0 199 | occluder.position = positions[i] 200 | occluder.rotate(rot_axis[i / 4], rot_angle[i]) 201 | add_child(occluder) 202 | occluders.append(occluder) 203 | 204 | func _notification(what): 205 | match what: 206 | NOTIFICATION_TRANSFORM_CHANGED: 207 | refresh() 208 | NOTIFICATION_VISIBILITY_CHANGED: 209 | if visual is RID: 210 | RenderingServer.instance_set_visible(visual, is_visible_in_tree()) 211 | 212 | func _enter_tree() -> void: 213 | refresh() 214 | 215 | func _exit_tree() -> void: 216 | clear() 217 | -------------------------------------------------------------------------------- /addons/level_block/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Level Block" 4 | description="A node for creating simple cubes with customizable sides and collision. Intended for dungeon crawler level design." 5 | author="Reun Media" 6 | version="1.0.2" 7 | script="level_block.gd" 8 | -------------------------------------------------------------------------------- /addons/level_block/src/navmesh_parser.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | 3 | const LevelBlockNode = preload("res://addons/level_block/level_block_node.gd") 4 | 5 | var source_geometry_parser: RID 6 | var source_geometry_parser_callback: Callable 7 | 8 | func create_parser() -> void: 9 | source_geometry_parser_callback = Callable( 10 | self, 11 | "_on_source_geometry_parser_callback" 12 | ) 13 | source_geometry_parser = NavigationServer3D.source_geometry_parser_create() 14 | NavigationServer3D.source_geometry_parser_set_callback( 15 | source_geometry_parser, 16 | source_geometry_parser_callback 17 | ) 18 | 19 | func delete_parser() -> void: 20 | NavigationServer3D.free_rid(source_geometry_parser) 21 | source_geometry_parser = RID() 22 | source_geometry_parser_callback = Callable() 23 | 24 | func _on_source_geometry_parser_callback( 25 | p_navigation_mesh: NavigationMesh, 26 | p_source_geometry_data: NavigationMeshSourceGeometryData3D, 27 | p_parsed_node: Node) -> void: 28 | 29 | # Skip navmesh generation for nodes that are not LevelBlock nodes 30 | if not p_parsed_node is LevelBlockNode: 31 | return 32 | 33 | # Skip navmesh generation for LevelBlocks without collision 34 | if not p_parsed_node.generate_collision: 35 | return 36 | 37 | # Skip navmesh generation for LevelBlocks with no faces 38 | if p_parsed_node.mesh_faces.size() == 0: 39 | return 40 | 41 | # Skip navmesh generation for LevelBlocks that are outside the 42 | # navigation mesh filter baking AABB 43 | var filter_baking_aabb := p_navigation_mesh.filter_baking_aabb 44 | if filter_baking_aabb.has_volume() && p_parsed_node.mesh_aabb.has_volume(): 45 | # Offset the mesh AABB by the filter baking AABB offset 46 | var filter_baking_aabb_offset := p_navigation_mesh.get_filter_baking_aabb_offset() 47 | filter_baking_aabb.position += filter_baking_aabb_offset 48 | 49 | # Convert the mesh AABB to global space with LevelBlock transfrom 50 | var mesh_aabb: AABB = p_parsed_node.global_transform * p_parsed_node.mesh_aabb 51 | 52 | if not filter_baking_aabb.intersects(mesh_aabb): 53 | return 54 | 55 | p_source_geometry_data.add_faces( 56 | p_parsed_node.mesh_faces, 57 | p_parsed_node.global_transform 58 | ) 59 | -------------------------------------------------------------------------------- /addons/level_block/texture_selector.gd: -------------------------------------------------------------------------------- 1 | extends EditorProperty 2 | 3 | var property_control = preload("res://addons/level_block/texture_selector_scene.tscn").instantiate() 4 | var clear_image := preload("res://addons/level_block/clear.svg") 5 | 6 | var current_value := 0 7 | var updating := false 8 | var texture_sheet : Texture2D 9 | var texture_size : float 10 | 11 | var texture 12 | var value 13 | 14 | func _init(): 15 | add_child(property_control) 16 | texture = property_control.get_node("TextureRect") 17 | value = property_control.get_node("SpinBox") 18 | add_focusable(property_control) 19 | value.connect("value_changed", Callable(self, "update_value")) 20 | refresh_control() 21 | 22 | func update_value(new_value: float): 23 | if updating: 24 | return 25 | current_value = new_value 26 | refresh_control() 27 | emit_changed(get_edited_property(), current_value) 28 | 29 | func _update_property(): 30 | var new_value = get_edited_object()[get_edited_property()] 31 | if (new_value == current_value): 32 | refresh_control() 33 | return 34 | updating = true 35 | current_value = new_value 36 | value.value = new_value 37 | refresh_control() 38 | updating = false 39 | 40 | func update_texture(new_texture: Texture2D): 41 | texture_sheet = new_texture 42 | refresh_control() 43 | 44 | func update_texture_size(new_size: float): 45 | texture_size = new_size 46 | refresh_control() 47 | 48 | func refresh_control(): 49 | if not texture_sheet is Texture2D: 50 | return 51 | if current_value < 0: 52 | texture.texture = clear_image 53 | return 54 | texture.texture = AtlasTexture.new() 55 | texture.texture.atlas = texture_sheet.duplicate() 56 | # Texture flags have been moved to nodes in Godot 4 57 | var pos = Vector2.ZERO 58 | var gap = texture_size / texture_sheet.get_size().x 59 | pos.x = fmod(gap * current_value, 1.0) * texture_sheet.get_size().x 60 | pos.y = floor(current_value / (1.0 / gap)) * gap * texture_sheet.get_size().y 61 | texture.texture.region.position = pos 62 | texture.texture.region.size = Vector2(texture_size, texture_size) 63 | -------------------------------------------------------------------------------- /addons/level_block/texture_selector_scene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cxtfndjngv4jf"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://da5akf336dc8" path="res://addons/level_block/clear.svg" id="1"] 4 | 5 | [node name="TextureSelector" type="HBoxContainer"] 6 | size_flags_horizontal = 0 7 | size_flags_vertical = 0 8 | 9 | [node name="TextureRect" type="TextureRect" parent="."] 10 | texture_filter = 1 11 | texture_repeat = 2 12 | custom_minimum_size = Vector2(48, 48) 13 | layout_mode = 2 14 | texture = ExtResource("1") 15 | expand_mode = 1 16 | stretch_mode = 5 17 | 18 | [node name="SpinBox" type="SpinBox" parent="."] 19 | layout_mode = 2 20 | size_flags_vertical = 4 21 | min_value = -1.0 22 | max_value = 255.0 23 | rounded = true 24 | allow_greater = true 25 | --------------------------------------------------------------------------------