Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f6383be
Create tileset resource
Koopa1018 Feb 24, 2024
06f9c4a
Rename Tileset to TerrainSkin
Koopa1018 May 27, 2024
4ba4af6
Integrate skin resource into Terrain
Koopa1018 May 27, 2024
d67655d
Rename tilesets folder to terrain_skins
Koopa1018 May 27, 2024
49177cc
Clear textures when no terrain skin is set
Koopa1018 May 27, 2024
451f438
Rename things in terrain border generators
Koopa1018 May 27, 2024
4348f64
Create simple grass tileset
Koopa1018 May 27, 2024
31ecce1
Re-add root node texture variables
Koopa1018 May 27, 2024
1ce3b04
Rename shadow textures to "clip"
Koopa1018 May 27, 2024
f8a7b59
Force terrain redraw on tileset change
Koopa1018 May 27, 2024
5db3093
Emit changed signal when skin is modified
Koopa1018 May 27, 2024
591353d
Register terrain to terrain skin's change signal
Koopa1018 May 27, 2024
b2e4beb
Make TerrainSkin properly call emit_changed
Koopa1018 May 27, 2024
3e055c0
Don't update borders if border node is null
Koopa1018 May 27, 2024
be467be
Don't draw terrain parts with no textures
Koopa1018 May 27, 2024
c00cc75
Create importer plugin
Koopa1018 May 27, 2024
831afa2
Rename Tileset to TerrainTemplate in importer
Koopa1018 May 28, 2024
e00042e
Fill out import function
Koopa1018 May 28, 2024
a3434ba
Give names to terrain texture subresources
Koopa1018 May 29, 2024
32a3fb8
Make texture slice regions configurable
Koopa1018 May 29, 2024
36afe56
Don't create textures when size is 0
Koopa1018 May 29, 2024
040e63a
Don't create texture when all alpha is 0
Koopa1018 May 29, 2024
00cbd2e
Replace temp resource with imported spritesheet
Koopa1018 May 29, 2024
aa1c6c9
Deduplicate tex-slice-creation code
Koopa1018 May 29, 2024
b292c9a
Formally update project to Godot 4.2
Koopa1018 May 29, 2024
1ba39f7
Merge branch 'repo-godot-4-2' into feature-tileset-template-importer
Koopa1018 May 29, 2024
2e45d3f
Add option to store terrain textures separate
Koopa1018 May 29, 2024
686a99c
Don't overwrite externally modified textures
Koopa1018 May 29, 2024
63a1161
Use output resource's pre-set path in save
Koopa1018 May 29, 2024
80d47a1
Save grass skin's external-textures preference
Koopa1018 May 29, 2024
34f7aaa
Hide top clip texture when it's empty
Koopa1018 May 29, 2024
772ea2c
Reorder terrain skin rect imports
Koopa1018 May 30, 2024
8f171a6
Reattach grass texture
Koopa1018 Jun 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions addons/tileset_from_image/plugin.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[plugin]

name="Terrain Skin from Template Image"
description="Automatically creates terrain skins from multi-texture template images."
author="Redux Team"
version="0.0.1"
script="plugin.gd"
15 changes: 15 additions & 0 deletions addons/tileset_from_image/plugin.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@tool
extends EditorPlugin

# Declared here so it can be cleaned up in _exit_tree().
var plugin


func _enter_tree():
# Load and instantiate the actual script.
plugin = preload("res://addons/tileset_from_image/tileset_from_image.gd").new()
add_import_plugin(plugin)


func _exit_tree():
remove_import_plugin(plugin)
242 changes: 242 additions & 0 deletions addons/tileset_from_image/tileset_from_image.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
@tool
extends EditorImportPlugin

enum Presets {
## The format originally designed for the Tiny Demo.
FORMAT_V1
}


## Returns the number of presets.
func _get_preset_count():
return Presets.size()


## Returns the user-facing names of the indexed preset.
func _get_preset_name(preset_index):
match preset_index:
Presets.FORMAT_V1:
return "Tiny Demo-era"
_:
return "Unknown"


## Returns the actual parameters of the indexed preset.
func _get_import_options(path, preset_index):
match preset_index:
Presets.FORMAT_V1:
return [
{
"name": "external_textures",
"default_value": false
},
{
"name": "Texture Rects",
"default_value": null,
"usage": PROPERTY_USAGE_GROUP,
},
{
"name": "body",
"default_value": Rect2(36, 3, 32, 32)
},
{
"name": "top",
"default_value": Rect2(105, 3, 32, 32)
},
{
"name": "top_endcap",
"default_value": Rect2(72, 3, 32, 32)
},
{
"name": "top_clip",
"default_value": Rect2(105, 36, 32, 32)
},
{
"name": "top_endcap_clip",
"default_value": Rect2(72, 36, 32, 32)
},
{
"name": "side",
"default_value": Rect2(3, 3, 32, 32)
},
{
"name": "bottom",
"default_value": Rect2(36, 36, 32, 32)
},

]
_: #
return []


func _get_importer_name():
return "63r.terrain_skin"


func _get_visible_name():
return "Terrain Skin Template"


func _get_recognized_extensions():
return ["bmp", "png", "jpg", "jpeg", "tga", "webp"]


func _get_save_extension():
return "res"


func _get_resource_type():
return "Resource"


func _get_import_order():
return 0

## Returns true for any option which is visible.
func _get_option_visibility(path, option_name, options):
return true # Stubbed while no options exist.


func _import(source_file, save_path, options, r_platform_variants, r_gen_files):
# Open the given file (and error if invalid).
var file := FileAccess.get_file_as_bytes(source_file)
if file == null:
return FileAccess.get_open_error()

# Load image data from the bytes.
var img := Image.new()
var parse_result
match source_file.get_extension().to_lower() :
"bmp":
parse_result = img.load_bmp_from_buffer(file)
"png":
parse_result = img.load_png_from_buffer(file)
"jpg", "jpeg":
# Not an ideal format, but heck, no reason to prevent it
parse_result = img.load_jpg_from_buffer(file)
"tga":
parse_result = img.load_tga_from_buffer(file)
"webp": # why
parse_result = img.load_webp_from_buffer(file)
_:
parse_result = ERR_INVALID_DATA
# Abort if the load failed.
if parse_result != OK:
return parse_result

# Create output file.
var out_res := TerrainSkin.new()

# Get the raw source filename.
var in_extension = source_file.get_extension()
var tex_name = source_file.get_file()
# Remove the extension from the end of name.
tex_name = tex_name.erase(
tex_name.length() - (in_extension.length() + 1),
in_extension.length() + 1
)

out_res.resource_name = tex_name
out_res.resource_path = "%s.%s" % [save_path, _get_save_extension()]

# Needed when "External Textures" is checked.
var tex_folder: String
var editor_fs: EditorFileSystem
if options["external_textures"]:
# Create a folder to store the separated texture resources.
tex_folder = "%s/%s" % [source_file.get_base_dir(), tex_name]
DirAccess.make_dir_absolute(tex_folder)

# Get access to the editor filesystem--need this to make the editor
# acknowledge newly created files.
editor_fs = EditorInterface.get_resource_filesystem()

# Make the editor acknowledge the textures directory.
editor_fs.update_file(tex_folder)


# Slice each non-zero-sized texture from the spritesheet.
# (Can't just use atlas textures, they don't loop like we need.)
for tex_type in [
"body",
"top", "top_clip",
"top_endcap", "top_endcap_clip",
"side", "bottom"
]:
if options[tex_type].size.x > 0 and options[tex_type].size.y > 0:
var slice = img.get_region(options[tex_type])
# If the slice has any visible pixels...
if !slice.is_invisible():
# ...create a texture from it.
var tex = ImageTexture.create_from_image(slice)

# Name the texture resource.
tex.resource_name = "%s_%s" % [tex_name, tex_type]

# If we want separate texture files, save those.
if options["external_textures"]:
var tex_path = "%s/%s.png" % \
[tex_folder, tex_type]

# If the file's been modified outside the importer,
# do not save over it.
var overwrite_safe := true
if ResourceLoader.exists(tex_path):
var existing = ResourceLoader.load(tex_path)

# Check when the imported resource was last modified.
# If it exists. Otherwise it's 0.
var last_imp_time = 0
if FileAccess.file_exists(out_res.resource_path):
last_imp_time = FileAccess.get_modified_time(out_res.resource_path)

# Check the texture's current modify time.
var modified_time = FileAccess.get_modified_time(tex_path)

# If the texture's modify time is unset or mismatches
# the resource file, the texture has been modified
# externally and is not safe to replace.
if modified_time == 0 or modified_time != last_imp_time:
overwrite_safe = false
push_warning(
"%s's %s texture appears to have been externally modified. Skipping over it."
% [
tex_name, tex_type.capitalize()
]
)

# If it's safe to overwrite, save this texture into that folder.
if overwrite_safe:
# Write the texture.
var save_result = ResourceSaver.save(tex, tex_path)
if save_result != OK:
push_error("Failed to write to %s: %s" % [
tex_path,
error_string(save_result),
])
continue

# Tell the editor this file exists.
editor_fs.update_file(tex_path)

# Try importing the sliced texture.
var imp_result = append_import_external_resource(tex_path)
# If import failed, report and leave the texture blank.
if imp_result != OK:
push_error("%s's %s texture failed to import: %s" % [
tex_name, tex_type.capitalize(),
error_string(imp_result)
])
continue

# Register this as one of the generated files.
r_gen_files.push_back(tex_path)

# Separate or embedded, save it to the resource.
out_res.set(tex_type, tex)

# If there's different variants of this resource for different platforms,
# push the feature tag to r_platform_variants, then insert the tag between
# save_path and _get_save_extension() (. separated) when saving the files.

return ResourceSaver.save(out_res)
53 changes: 32 additions & 21 deletions classes/solid/terrain/terrain_border.gd
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,35 @@ enum EdgeType {

const QUAD_RADIUS = 16

@onready var root = $".."
@onready var root: TerrainPolygon = $".."
@onready var body_polygon: Polygon2D = $"../Body"
@onready var top_edges: TerrainBorderEndcaps = $"../TopEdgeEndcaps"
@onready var endcaps: TerrainBorderEndcaps = $"../TopEdgeEndcaps"


func _draw():
# Clear polygons created last time we updated.
for child in get_children():
child.queue_free()

# Load appearance from the root.
body_polygon.texture = root.body
body_polygon.polygon = root.polygon
body_polygon.color = root.tint_color if root.tint else Color(1, 1, 1)
# Load body appearance from the root, if there's a body texture.
if root.body != null:
body_polygon.visible = true
body_polygon.texture = root.body
body_polygon.polygon = root.polygon
body_polygon.color = root.tint_color if root.tint else Color(1, 1, 1)
# If the root has no body texture, hide the body polygon.
else:
body_polygon.visible = false

# Clear the draw queue for top edges
top_edges.area_queue = []
# Clear the draw queue for endcaps.
endcaps.area_queue = []
# Draw all terrain polygons.
add_full(root.polygon)
# Queue drawing the top edges.
top_edges.queue_redraw()
draw_all_borders(root.polygon)
# Queue drawing the endcaps.
endcaps.queue_redraw()


func add_full(poly: PackedVector2Array):
func draw_all_borders(poly: PackedVector2Array):
# Dictionary of segments which have had their type ID evaluated.
# Types are indexed by first vertex: overrides[3] will return the
# type ID of segment (3, 4).
Expand All @@ -60,7 +65,7 @@ func add_full(poly: PackedVector2Array):
# Valid chains contain at least 2 vertices.
# If the chain is valid, draw it.
if list.size() >= 2:
generate_polygons(list, root.edge, 0)
generate_polygons(list, root.side, 0)

# Now the bottom as well--same exact deal as the sides.
latest_index = 0
Expand Down Expand Up @@ -157,7 +162,7 @@ func generate_polygons_top(lines, z_order = 2):

# Draw everything
# Mark the left-side area for drawing.
top_edges.area_queue.append([true, areas.front()])
endcaps.area_queue.append([true, areas.front()])

# Draw all areas.
for area in areas:
Expand All @@ -166,18 +171,20 @@ func generate_polygons_top(lines, z_order = 2):
0 if area.type == "quad" or (area.clock_dir == -1 and area.type == "trio")
else area.verts.size() - 2)

# Add it as a child.
add_child(poly2d)
# Add it as a child, if its texture exists.
if root.top != null:
add_child(poly2d)

# Make a duplicate polygon to hold the shadow texture.
# TODO: This may be meant to get clipped to within the polygon's body.
if !root.tint:
# Make a duplicate polygon to hold the clip texture, if any.
# This texture is meant to get clipped to within the polygon's body.
# TODO: Is it doing that though?
if !root.tint and root.top_clip != null:
var shade = poly2d.duplicate()
shade.texture = root.top_shade
shade.texture = root.top_clip
add_child(shade)

# Mark the right-side area for drawing.
top_edges.area_queue.append([false, areas.back()])
endcaps.area_queue.append([false, areas.back()])


func _add_inbetween_segment(areas, start: Vector2, end: Vector2, circumcenter: Vector2):
Expand Down Expand Up @@ -227,6 +234,10 @@ func _add_inbetween_segment(areas, start: Vector2, end: Vector2, circumcenter: V


func generate_polygons(lines: Array, texture: Texture2D, z_order: int):
# If this chain of polygons has no texture set, completely skip it.
if texture == null:
return

var p_len = lines.size()
for ind in range(p_len - 1):
# First create quads from each line segment.
Expand Down
Loading